@atscript/moost-ui-presets
Moost controller + atscript schema for table-preset persistence. Subclass AsPresetsController to wire the table to your auth layer; the rest (CRUD, dirty detection, public-label uniqueness, per-user quotas, capabilities endpoint) ships out of the box. Pair with @atscript/vue-table's usePresets() / <AsPresetPicker> to ship full preset UX.
Contents
Controller
AsPresetsController<T?>
Abstract base extending AsDbController from @atscript/moost-db. Inherits the standard CRUD endpoints (GET, POST, PATCH, DELETE) and adds:
GET /capabilities?app=...&tableKey=...— returns the user's preset capabilities (publish rights, quota).- Read gating —
transformFilterinjects an owner-aware filter so users only see their own private rows and everyone's public rows. - Write hooks —
onWriteenforces label uniqueness, quota, and stampsuserLabel/publicLabel/aspects. - Delete gating —
onRemoveonly allows owners to delete.
@Inherit()
abstract class AsPresetsController<
T extends TAtscriptAnnotatedType = typeof AsPresetEntry,
> extends AsDbController<T> {
protected maxPresetsPerUser: number; // 10
protected abstract getCurrentUser(): Promise<string>;
protected getMaxPresetsPerUser(
app: string,
tableKey: string,
user: string,
): Promise<number>;
protected canPublishPresets(
app: string,
tableKey: string,
user: string,
): Promise<boolean>;
protected getUserLabel(user: string): Promise<string | undefined>;
@Get("capabilities")
capabilities(query: AsCapabilitiesQuery): Promise<PresetCapabilities>;
}Implementation example
import { Controller } from "moost";
import { AsPresetsController } from "@atscript/moost-ui-presets";
@Controller("/presets")
class PresetsController extends AsPresetsController {
constructor(private readonly auth: AuthService) {
super();
}
protected async getCurrentUser(): Promise<string> {
const user = await this.auth.currentUser();
if (!user) throw new HttpError(401, "Unauthorized");
return user.id;
}
/** Free tier: 10 presets. Paid: 100. Admin: unlimited. */
protected async getMaxPresetsPerUser(app: string, _table: string, user: string) {
const tier = await this.auth.tierOf(user);
return tier === "admin" ? Number.MAX_SAFE_INTEGER : tier === "paid" ? 100 : 10;
}
protected async canPublishPresets(_app: string, _table: string, user: string) {
return (await this.auth.tierOf(user)) !== "free";
}
protected async getUserLabel(user: string) {
return (await this.auth.profileOf(user))?.displayName;
}
}Extension points
| Method | Default | Override when |
|---|---|---|
getCurrentUser() | abstract | Always — wire to your auth layer. |
getMaxPresetsPerUser(app, tableKey, user) | returns this.maxPresetsPerUser (10) | Tiered quotas (admin / paid / free). |
canPublishPresets(app, tableKey, user) | returns true | Restrict public presets by tier or per-table policy. |
getUserLabel(user) | returns undefined | Surface display names on shared presets. |
REST endpoints
All endpoints inherited from AsDbController work with AsPresetEntry rows. The controller adds:
GET /capabilities?app=<app>&tableKey=<tableKey>→PresetCapabilities.
The @atscript/ui-table PresetsClient targets this exact controller.
Schema — AsPresetEntry
The atscript-db row shape. Stored in table as_presets.
@db.table 'as_presets'
export interface AsPresetEntry {
@meta.id
@db.default.uuid
id: string
/** Row kind — drives the polymorphic `data` column. */
type: 'preset' | 'userConf' | 'appConf'
/** App scope. Required on every row. */
app: string
/** Per-table scope. Required for `preset` / `userConf`; absent for `appConf`. */
tableKey?: string
/** Owner user id. Stamped by the controller from session. */
user: string
/** Optional display name for shared presets. */
userLabel?: string
/** Visible to other users when true. Only meaningful for `type='preset'`. */
public?: boolean
/** Mirror of `data.label` — top-level for indexable lookups. `type='preset'` only. */
label?: string
/** Race-safe public-name uniqueness column. `type='preset' AND public=true` only. */
publicLabel?: string
/** Aspects this preset claims — derived by the controller on every write. */
aspects?: PresetAspect[]
@db.json
data: PresetData | UserConfData | AppConfData
@db.default.now createdAt: number
@db.default.now updatedAt: number
}Polymorphic data shape
type='preset'→PresetData:{ label, content?: PresetSnapshotWire }. The snapshot blob carriescolumns,filters,filterOps,sorters,itemsPerPage.type='userConf'→UserConfData:{ defaultPresetId?, favPresetIds? }. Per-table user preferences.type='appConf'→AppConfData:{ appearance?, language?, timezone?, density?, dateFormat?, firstDayOfWeek?, customJson? }. App-wide per-user preferences (one row per(user, app)).
Validation rules enforced by the controller
aspects[]is rebuilt fromdata.contentkeys on every write — clients can't fake aspect claims.labelis mirrored fromdata.labelon everytype='preset'write.publicLabelis stamped equal tolabelontype='preset' AND public=truewrites, NULL otherwise. The composite unique indexpreset_public_label_idx (app, tableKey, publicLabel)relies on NULL ≠ NULL semantics so private / userConf / appConf rows never collide.useris stamped fromgetCurrentUser()on every write — clients can't author rows under another user.- Per-user quota is checked on insert; exceeding it returns HTTP
409withcode: "preset_limit_reached"and{ limit, count }in the body.
Re-exports
From @atscript/ui-table — re-exported so consumers can grab schema-relevant types from one import:
export {
// Types
type AppConfData,
type AsPresetEntryData,
type AsPresetsErrorCode,
type FilterCondition,
type FilterConditionType,
type PresetAspect,
type PresetCapabilities,
type PresetData,
type PresetLimitReachedBody,
type PresetSnapshotWire,
type UserConfData,
// Constants
APP_CONF_PREFIX,
PRESET_ASPECTS,
RESERVED_ID_PREFIXES,
SYSTEM_PRESET_PREFIX,
USER_CONF_PREFIX,
// Helpers
appConfId,
userConfId,
} from "@atscript/ui-table";appConfId(user, app) and userConfId(user, app, tableKey) are the deterministic id builders the controller uses for the singleton appConf and per-table userConf rows.
Cross-links
- Tables — Presets, Server-Side Presets
- @atscript/ui-table — wire types, draft helpers,
PresetsClient - @atscript/vue-table —
usePresets(),<AsPresetPicker>,<AsPresetDialog> - db.atscript.dev — Tables — base CRUD endpoints inherited from
AsDbController - atscript.dev —
.assyntax,@db.*annotations