rastack 0.0.50 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import * as ts from "typescript";
2
2
  import { ResourceModel } from "./model";
3
3
  import type { EntityTypeExport } from "./entities";
4
+ export { applyTransitions } from "../define/manifest";
4
5
  /**
5
6
  * Statically analyse resource definitions and produce the canonical model.
6
7
  *
@@ -21,13 +22,6 @@ export declare function analyzeDsl(program: ts.Program, fileNames: string[]): {
21
22
  resources: ResourceModel[];
22
23
  types: EntityTypeExport[];
23
24
  };
24
- /**
25
- * Attach a normalised `transitions` block to a resource and derive what the
26
- * machine implies about its state field: the `states` become the field's
27
- * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
28
- * `initial` becomes its default when the field doesn't declare one.
29
- */
30
- export declare function applyTransitions(resource: ResourceModel, raw: unknown): void;
31
25
  /**
32
26
  * Evaluate a *static literal* AST node (string/number/bool/null/array/object)
33
27
  * into a JS value. Anything non-literal (a function, an identifier) yields
@@ -33,14 +33,20 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.applyTransitions = void 0;
36
37
  exports.analyze = analyze;
37
38
  exports.analyzeDsl = analyzeDsl;
38
- exports.applyTransitions = applyTransitions;
39
39
  exports.literalToValue = literalToValue;
40
40
  const path = __importStar(require("path"));
41
41
  const ts = __importStar(require("typescript"));
42
42
  const program_1 = require("./program");
43
- const transitions_1 = require("../validate/transitions");
43
+ const manifest_1 = require("../define/manifest");
44
+ // The option normalisers live beside the runtime manifest builder
45
+ // (`define/manifest.ts`) and are shared with it, so a manifest built from
46
+ // runtime `resource()` values and one compiled from the same source can
47
+ // never disagree. Re-exported here for existing importers.
48
+ var manifest_2 = require("../define/manifest");
49
+ Object.defineProperty(exports, "applyTransitions", { enumerable: true, get: function () { return manifest_2.applyTransitions; } });
44
50
  /**
45
51
  * Statically analyse resource definitions and produce the canonical model.
46
52
  *
@@ -139,35 +145,16 @@ function analyzeResourceCall(call, checker) {
139
145
  model,
140
146
  fields,
141
147
  relations,
142
- sync: normaliseSync(options.sync),
143
- admin: normaliseAdmin(options.admin),
148
+ sync: (0, manifest_1.normaliseSync)(options.sync),
149
+ admin: (0, manifest_1.normaliseAdmin)(options.admin),
144
150
  search: options.search,
145
151
  ordering: options.ordering,
146
152
  permission: options.permission,
147
- access: normaliseAccess(options.access),
153
+ access: (0, manifest_1.normaliseAccess)(options.access),
148
154
  };
149
- applyTransitions(resource, options.transitions);
155
+ (0, manifest_1.applyTransitions)(resource, options.transitions);
150
156
  return resource;
151
157
  }
152
- /**
153
- * Attach a normalised `transitions` block to a resource and derive what the
154
- * machine implies about its state field: the `states` become the field's
155
- * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
156
- * `initial` becomes its default when the field doesn't declare one.
157
- */
158
- function applyTransitions(resource, raw) {
159
- const transitions = (0, transitions_1.normalizeTransitions)(raw);
160
- if (!transitions)
161
- return;
162
- resource.transitions = transitions;
163
- const stateField = resource.fields.find((f) => f.name === transitions.field);
164
- if (stateField && stateField.type !== "fk") {
165
- stateField.options = [...transitions.states];
166
- if (stateField.default === undefined && transitions.initial !== undefined) {
167
- stateField.default = transitions.initial;
168
- }
169
- }
170
- }
171
158
  /**
172
159
  * Resolve a single field from its initializer. FK detection is type-driven:
173
160
  * the initializer's *type* is inspected, and a `Ref` type yields a foreign key.
@@ -278,46 +265,3 @@ function literalToValue(node) {
278
265
  }
279
266
  return undefined;
280
267
  }
281
- function normaliseSync(sync) {
282
- if (!sync || typeof sync !== "object")
283
- return undefined;
284
- const mode = sync.mode === "local" ? "local" : "server";
285
- if (mode === "server" && !sync.confirmed) {
286
- // A bare server resource carries no sync semantics.
287
- return undefined;
288
- }
289
- const model = { mode };
290
- if (typeof sync.scope === "string")
291
- model.scope = sync.scope;
292
- if (Array.isArray(sync.confirmed))
293
- model.confirmed = sync.confirmed;
294
- if (sync.load === "lazy")
295
- model.load = "lazy";
296
- if (sync.clientIds === true)
297
- model.clientIds = true;
298
- return model;
299
- }
300
- function normaliseAccess(access) {
301
- if (!access || typeof access !== "object")
302
- return undefined;
303
- const model = {};
304
- if (typeof access.ownerField === "string")
305
- model.ownerField = access.ownerField;
306
- if (access.scope === "owner" || access.scope === "shared") {
307
- model.scope = access.scope;
308
- }
309
- if (Array.isArray(access.adminGroups)) {
310
- model.adminGroups = access.adminGroups.filter((g) => typeof g === "string");
311
- }
312
- return Object.keys(model).length ? model : undefined;
313
- }
314
- function normaliseAdmin(admin) {
315
- if (admin === false)
316
- return false;
317
- if (!admin || typeof admin !== "object")
318
- return undefined;
319
- const model = {};
320
- if (Array.isArray(admin.listDisplay))
321
- model.listDisplay = admin.listDisplay;
322
- return model;
323
- }
@@ -22,14 +22,31 @@
22
22
  * });
23
23
  * ```
24
24
  */
25
+ /**
26
+ * Every constraint a scalar builder may be handed, merged — the shape the
27
+ * runtime value carries so a manifest can be assembled without the compiler.
28
+ */
29
+ export interface ScalarFieldOptions {
30
+ maxLength?: number;
31
+ unique?: boolean;
32
+ primaryKey?: boolean;
33
+ default?: unknown;
34
+ null?: boolean;
35
+ autoNow?: boolean;
36
+ autoNowAdd?: boolean;
37
+ }
25
38
  /**
26
39
  * A scalar field. The literal `__rastackScalar` type is what the compiler
27
40
  * reads; `Nullable` phantom-carries the `null: true` option so {@link RowOf}
28
- * can add `| null` to the inferred row type.
41
+ * can add `| null` to the inferred row type. The authored options ride along
42
+ * on the runtime value too, so `manifestFromResources()` can build the same
43
+ * manifest `rastack compile` emits — with no compile step at all.
29
44
  */
30
45
  export interface Scalar<T extends string, Nullable extends boolean = boolean> {
31
46
  readonly __rastackScalar: T;
32
47
  readonly __rastackNullable?: Nullable;
48
+ /** The constraint options as authored — carried at runtime. */
49
+ readonly options?: ScalarFieldOptions;
33
50
  }
34
51
  /** Whether an options literal declares `null: true` (as a type). */
35
52
  type NullOf<O> = O extends {
@@ -37,12 +54,16 @@ type NullOf<O> = O extends {
37
54
  } ? true : false;
38
55
  /**
39
56
  * A foreign-key field. `App`/`Model` are carried as string-literal type
40
- * arguments so the compiler can recover the relation target purely from types.
57
+ * arguments so the compiler can recover the relation target purely from
58
+ * types; the target thunk is *also* kept on the runtime value so
59
+ * `manifestFromResources()` can recover the relation with no compiler.
41
60
  */
42
61
  export interface Ref<App extends string, Model extends string> {
43
62
  readonly __rastackRef: true;
44
63
  readonly __app: App;
45
64
  readonly __model: Model;
65
+ /** The authored `() => Resource` thunk — carried at runtime. */
66
+ readonly target?: () => ResourceType<App, Model, any>;
46
67
  }
47
68
  export interface StringOptions {
48
69
  maxLength?: number;
@@ -67,24 +88,24 @@ export interface DateOptions {
67
88
  null?: boolean;
68
89
  }
69
90
  /**
70
- * Field builders. Each returns a phantom value whose *type* encodes the field
71
- * kind; the runtime object is never used at compile time (the compiler analyses
72
- * the source, it does not execute it) but is real so the DSL is also usable at
73
- * runtime if a consumer wants it.
91
+ * Field builders. Each returns a value whose *type* encodes the field kind
92
+ * (what the compiler reads — it analyses the source, it does not execute it)
93
+ * and whose runtime shape keeps the authored options/target, so the same
94
+ * definition doubles as its own manifest via `manifestFromResources()`.
74
95
  */
75
96
  export declare const s: {
76
- string<const O extends StringOptions = StringOptions>(_opts?: O): Scalar<"string", NullOf<O>>;
77
- int<const O extends NumberOptions = NumberOptions>(_opts?: O): Scalar<"int", NullOf<O>>;
78
- float<const O extends NumberOptions = NumberOptions>(_opts?: O): Scalar<"float", NullOf<O>>;
79
- bool<const O extends BoolOptions = BoolOptions>(_opts?: O): Scalar<"bool", NullOf<O>>;
80
- datetime<const O extends DateOptions = DateOptions>(_opts?: O): Scalar<"datetime", NullOf<O>>;
81
- uuid<const O extends StringOptions = StringOptions>(_opts?: O): Scalar<"uuid", NullOf<O>>;
97
+ string<const O extends StringOptions = StringOptions>(opts?: O): Scalar<"string", NullOf<O>>;
98
+ int<const O extends NumberOptions = NumberOptions>(opts?: O): Scalar<"int", NullOf<O>>;
99
+ float<const O extends NumberOptions = NumberOptions>(opts?: O): Scalar<"float", NullOf<O>>;
100
+ bool<const O extends BoolOptions = BoolOptions>(opts?: O): Scalar<"bool", NullOf<O>>;
101
+ datetime<const O extends DateOptions = DateOptions>(opts?: O): Scalar<"datetime", NullOf<O>>;
102
+ uuid<const O extends StringOptions = StringOptions>(opts?: O): Scalar<"uuid", NullOf<O>>;
82
103
  /**
83
104
  * A foreign key. The target is `() => SomeResource`; the compiler resolves
84
- * the relation from the *type* `Ref<App, Model>` this returns — it does not
85
- * need to run the thunk.
105
+ * the relation from the *type* `Ref<App, Model>` this returns, and the
106
+ * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
86
107
  */
87
- ref<A extends string, M extends string>(_thunk: () => ResourceType<A, M, any>): Ref<A, M>;
108
+ ref<A extends string, M extends string>(thunk: () => ResourceType<A, M, any>): Ref<A, M>;
88
109
  };
89
110
  export type AnyField = Scalar<string> | Ref<string, string>;
90
111
  /** Local-first sync configuration (mirrors the old Python `class Sync:`). */
@@ -229,4 +250,30 @@ export type RowOf<R> = R extends ResourceType<string, string, infer F, ResourceO
229
250
  } & {
230
251
  [K in keyof F]: FieldValueOf<F[K]>;
231
252
  } : never;
232
- export {};
253
+ /** The `"app.model"` registry key a `resource()` definition implies. */
254
+ export type EntityKeyOf<R> = R extends ResourceType<infer A, infer M, any, ResourceOptions> ? `${A}.${M}` : never;
255
+ /**
256
+ * The `EntityTypes` registry entries a module of `resource()` definitions
257
+ * implies — the *hand-written* alternative to the `rastack-env.d.ts` that
258
+ * `rastack compile` emits. Point it at your resources module once and the
259
+ * registry tracks it forever, with no compile step in the loop:
260
+ *
261
+ * ```ts
262
+ * // rastack-env.d.ts — written once by you, never regenerated
263
+ * import type { EntityTypesOf } from "rastack/define";
264
+ * declare module "rastack/hooks/registry" {
265
+ * interface EntityTypes
266
+ * extends EntityTypesOf<typeof import("./resources/airports")> {}
267
+ * }
268
+ * ```
269
+ *
270
+ * Non-resource exports are dropped, so `typeof import("…")` over a whole
271
+ * module is safe. (Class entities carry their own row type — they never need
272
+ * the registry; interfaces still register via the compile-emitted file.)
273
+ */
274
+ export type EntityTypesOf<M> = {
275
+ [K in keyof M as EntityKeyOf<M[K]>]: RowOf<M[K]>;
276
+ };
277
+ export { isResourceValue, manifestFromResources, resourceModel, toManifest, } from "./manifest";
278
+ export type { AnyResourceValue, ManifestInput } from "./manifest";
279
+ export type { Manifest, ResourceModel } from "../compile/model";
@@ -24,40 +24,40 @@
24
24
  * ```
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.s = void 0;
27
+ exports.toManifest = exports.resourceModel = exports.manifestFromResources = exports.isResourceValue = exports.s = void 0;
28
28
  exports.resource = resource;
29
29
  /**
30
- * Field builders. Each returns a phantom value whose *type* encodes the field
31
- * kind; the runtime object is never used at compile time (the compiler analyses
32
- * the source, it does not execute it) but is real so the DSL is also usable at
33
- * runtime if a consumer wants it.
30
+ * Field builders. Each returns a value whose *type* encodes the field kind
31
+ * (what the compiler reads — it analyses the source, it does not execute it)
32
+ * and whose runtime shape keeps the authored options/target, so the same
33
+ * definition doubles as its own manifest via `manifestFromResources()`.
34
34
  */
35
35
  exports.s = {
36
- string(_opts) {
37
- return { __rastackScalar: "string" };
36
+ string(opts) {
37
+ return { __rastackScalar: "string", options: opts };
38
38
  },
39
- int(_opts) {
40
- return { __rastackScalar: "int" };
39
+ int(opts) {
40
+ return { __rastackScalar: "int", options: opts };
41
41
  },
42
- float(_opts) {
43
- return { __rastackScalar: "float" };
42
+ float(opts) {
43
+ return { __rastackScalar: "float", options: opts };
44
44
  },
45
- bool(_opts) {
46
- return { __rastackScalar: "bool" };
45
+ bool(opts) {
46
+ return { __rastackScalar: "bool", options: opts };
47
47
  },
48
- datetime(_opts) {
49
- return { __rastackScalar: "datetime" };
48
+ datetime(opts) {
49
+ return { __rastackScalar: "datetime", options: opts };
50
50
  },
51
- uuid(_opts) {
52
- return { __rastackScalar: "uuid" };
51
+ uuid(opts) {
52
+ return { __rastackScalar: "uuid", options: opts };
53
53
  },
54
54
  /**
55
55
  * A foreign key. The target is `() => SomeResource`; the compiler resolves
56
- * the relation from the *type* `Ref<App, Model>` this returns — it does not
57
- * need to run the thunk.
56
+ * the relation from the *type* `Ref<App, Model>` this returns, and the
57
+ * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
58
58
  */
59
- ref(_thunk) {
60
- return { __rastackRef: true };
59
+ ref(thunk) {
60
+ return { __rastackRef: true, target: thunk };
61
61
  },
62
62
  };
63
63
  /** Define a resource. Capture app/model (and options) as literal types. */
@@ -72,3 +72,9 @@ function resource(app, model, fields, options = {}) {
72
72
  options,
73
73
  };
74
74
  }
75
+ // The runtime manifest builder — a resource() definition is its own manifest.
76
+ var manifest_1 = require("./manifest");
77
+ Object.defineProperty(exports, "isResourceValue", { enumerable: true, get: function () { return manifest_1.isResourceValue; } });
78
+ Object.defineProperty(exports, "manifestFromResources", { enumerable: true, get: function () { return manifest_1.manifestFromResources; } });
79
+ Object.defineProperty(exports, "resourceModel", { enumerable: true, get: function () { return manifest_1.resourceModel; } });
80
+ Object.defineProperty(exports, "toManifest", { enumerable: true, get: function () { return manifest_1.toManifest; } });
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The runtime manifest builder — `resource()` definitions are their own
3
+ * manifest, **no `rastack compile` step required**.
4
+ *
5
+ * The DSL values carry everything the canonical manifest needs at runtime
6
+ * (field kinds, constraint options, FK target thunks, resource options), so
7
+ * this module assembles the exact `schema.rastack.json` shape the compiler
8
+ * emits — same fields, same relations, same normalisation — as a pure
9
+ * function:
10
+ *
11
+ * ```tsx
12
+ * import { Airport, Terminal, Flight } from "./resources/airports";
13
+ *
14
+ * <RAStackProvider mode="local" manifest={[Airport, Terminal, Flight]}>
15
+ * ```
16
+ *
17
+ * The compiler (`tools/src/compile/analyze.ts`) imports the option
18
+ * normalisers below, so the two paths cannot drift: `rastack compile` remains
19
+ * the way to get class/interface entities, `openapi.json`, and the artifacts
20
+ * the Rust backend and admin read — but the typed hooks and the in-browser
21
+ * engine need only this.
22
+ *
23
+ * Everything here is browser-safe: no `typescript`, no filesystem.
24
+ */
25
+ import type { AccessModel, AdminModel, Manifest, ResourceModel, SyncModel } from "../compile/model";
26
+ import type { ResourceType } from "./index";
27
+ /** Any `resource()` value, generics erased. */
28
+ export type AnyResourceValue = ResourceType<string, string, any, any>;
29
+ /** Everything `toManifest()` understands — see {@link toManifest}. */
30
+ export type ManifestInput = Manifest | object | string | AnyResourceValue | readonly AnyResourceValue[];
31
+ /** Whether a value is a `resource()` definition. */
32
+ export declare function isResourceValue(value: unknown): value is AnyResourceValue;
33
+ /**
34
+ * One `resource()` value → its canonical {@link ResourceModel} — the same
35
+ * shape `rastack compile` derives from the source, produced from the runtime
36
+ * value instead.
37
+ */
38
+ export declare function resourceModel(res: AnyResourceValue): ResourceModel;
39
+ /**
40
+ * Assemble the canonical manifest from `resource()` values at runtime —
41
+ * accepts the values themselves, arrays of them, or whole module namespaces
42
+ * (`import * as resources from "./resources"`), in any mix. Output order
43
+ * matches the compiler's (app, then model), so the result is byte-for-byte
44
+ * the manifest `rastack compile` would emit for the same definitions.
45
+ */
46
+ export declare function manifestFromResources(...inputs: Array<AnyResourceValue | readonly AnyResourceValue[] | Record<string, unknown>>): Manifest;
47
+ /**
48
+ * Normalise anything a provider may be handed as "the manifest": a compiled
49
+ * manifest (object or JSON string), a `resource()` value, an array of them,
50
+ * or a module namespace containing them. This is what lets
51
+ * `<RAStackProvider manifest={[Airport, Terminal]}>` work with no compiled
52
+ * `schema.rastack.json` anywhere.
53
+ */
54
+ export declare function toManifest(input: ManifestInput | null | undefined): Manifest | undefined;
55
+ /**
56
+ * Attach a normalised `transitions` block to a resource and derive what the
57
+ * machine implies about its state field: the `states` become the field's
58
+ * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
59
+ * `initial` becomes its default when the field doesn't declare one.
60
+ */
61
+ export declare function applyTransitions(resource: ResourceModel, raw: unknown): void;
62
+ export declare function normaliseSync(sync: any): SyncModel | undefined;
63
+ export declare function normaliseAccess(access: any): AccessModel | undefined;
64
+ export declare function normaliseAdmin(admin: any): AdminModel | false | undefined;
@@ -0,0 +1,250 @@
1
+ "use strict";
2
+ /**
3
+ * The runtime manifest builder — `resource()` definitions are their own
4
+ * manifest, **no `rastack compile` step required**.
5
+ *
6
+ * The DSL values carry everything the canonical manifest needs at runtime
7
+ * (field kinds, constraint options, FK target thunks, resource options), so
8
+ * this module assembles the exact `schema.rastack.json` shape the compiler
9
+ * emits — same fields, same relations, same normalisation — as a pure
10
+ * function:
11
+ *
12
+ * ```tsx
13
+ * import { Airport, Terminal, Flight } from "./resources/airports";
14
+ *
15
+ * <RAStackProvider mode="local" manifest={[Airport, Terminal, Flight]}>
16
+ * ```
17
+ *
18
+ * The compiler (`tools/src/compile/analyze.ts`) imports the option
19
+ * normalisers below, so the two paths cannot drift: `rastack compile` remains
20
+ * the way to get class/interface entities, `openapi.json`, and the artifacts
21
+ * the Rust backend and admin read — but the typed hooks and the in-browser
22
+ * engine need only this.
23
+ *
24
+ * Everything here is browser-safe: no `typescript`, no filesystem.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.isResourceValue = isResourceValue;
28
+ exports.resourceModel = resourceModel;
29
+ exports.manifestFromResources = manifestFromResources;
30
+ exports.toManifest = toManifest;
31
+ exports.applyTransitions = applyTransitions;
32
+ exports.normaliseSync = normaliseSync;
33
+ exports.normaliseAccess = normaliseAccess;
34
+ exports.normaliseAdmin = normaliseAdmin;
35
+ const transitions_1 = require("../validate/transitions");
36
+ /** Whether a value is a `resource()` definition. */
37
+ function isResourceValue(value) {
38
+ return (!!value &&
39
+ typeof value === "object" &&
40
+ value.__rastack === "resource");
41
+ }
42
+ /**
43
+ * One `resource()` value → its canonical {@link ResourceModel} — the same
44
+ * shape `rastack compile` derives from the source, produced from the runtime
45
+ * value instead.
46
+ */
47
+ function resourceModel(res) {
48
+ const fields = [];
49
+ const relations = [];
50
+ for (const [name, value] of Object.entries((res.fields ?? {}))) {
51
+ const field = runtimeField(res, name, value);
52
+ if (!field)
53
+ continue;
54
+ fields.push(field);
55
+ if (field.type === "fk" && field.relation) {
56
+ relations.push({
57
+ field: name,
58
+ target: `${field.relation.app}.${field.relation.model}`,
59
+ kind: "fk",
60
+ });
61
+ }
62
+ }
63
+ const options = (res.options ?? {});
64
+ const model = {
65
+ app: res.app,
66
+ model: res.model,
67
+ fields,
68
+ relations,
69
+ sync: normaliseSync(options.sync),
70
+ admin: normaliseAdmin(options.admin),
71
+ search: options.search,
72
+ ordering: options.ordering,
73
+ permission: options.permission,
74
+ access: normaliseAccess(options.access),
75
+ };
76
+ applyTransitions(model, options.transitions);
77
+ return model;
78
+ }
79
+ /** Resolve one runtime field value to its {@link FieldModel}. */
80
+ function runtimeField(res, name, value) {
81
+ if (!value || typeof value !== "object")
82
+ return undefined;
83
+ const field = value;
84
+ // Foreign key: the runtime value keeps the `() => Resource` thunk, so the
85
+ // relation target is recovered by calling it — the runtime twin of the
86
+ // compiler's "read the Ref<App, Model> type arguments" step.
87
+ if (field.__rastackRef === true) {
88
+ const target = field.target?.();
89
+ if (!isResourceValue(target)) {
90
+ throw new Error(`manifestFromResources: ${res.app}.${res.model}.${name} is an s.ref(...) whose target thunk did not return a resource() value`);
91
+ }
92
+ return {
93
+ name,
94
+ type: "fk",
95
+ relation: { app: target.app, model: target.model },
96
+ };
97
+ }
98
+ if (typeof field.__rastackScalar !== "string")
99
+ return undefined;
100
+ const model = {
101
+ name,
102
+ type: field.__rastackScalar,
103
+ };
104
+ // Constraint mapping mirrors the compiler's `applyConstraints` exactly.
105
+ const opts = field.options;
106
+ if (opts && typeof opts === "object") {
107
+ if (typeof opts.maxLength === "number")
108
+ model.maxLength = opts.maxLength;
109
+ if (opts.unique === true)
110
+ model.unique = true;
111
+ if (opts.primaryKey === true)
112
+ model.primaryKey = true;
113
+ if (opts.null === true)
114
+ model.null = true;
115
+ if (opts.default !== undefined)
116
+ model.default = opts.default;
117
+ }
118
+ return model;
119
+ }
120
+ /**
121
+ * Assemble the canonical manifest from `resource()` values at runtime —
122
+ * accepts the values themselves, arrays of them, or whole module namespaces
123
+ * (`import * as resources from "./resources"`), in any mix. Output order
124
+ * matches the compiler's (app, then model), so the result is byte-for-byte
125
+ * the manifest `rastack compile` would emit for the same definitions.
126
+ */
127
+ function manifestFromResources(...inputs) {
128
+ const resources = collectResources(inputs).map(resourceModel);
129
+ resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
130
+ return { version: 1, resources };
131
+ }
132
+ /** Flatten values / arrays / module namespaces into the resource list. */
133
+ function collectResources(inputs) {
134
+ const out = [];
135
+ const seen = new Set();
136
+ const push = (res) => {
137
+ if (!seen.has(res)) {
138
+ seen.add(res);
139
+ out.push(res);
140
+ }
141
+ };
142
+ for (const input of inputs) {
143
+ if (isResourceValue(input)) {
144
+ push(input);
145
+ }
146
+ else if (Array.isArray(input)) {
147
+ for (const item of input)
148
+ if (isResourceValue(item))
149
+ push(item);
150
+ }
151
+ else if (input && typeof input === "object") {
152
+ for (const item of Object.values(input)) {
153
+ if (isResourceValue(item))
154
+ push(item);
155
+ }
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+ /**
161
+ * Normalise anything a provider may be handed as "the manifest": a compiled
162
+ * manifest (object or JSON string), a `resource()` value, an array of them,
163
+ * or a module namespace containing them. This is what lets
164
+ * `<RAStackProvider manifest={[Airport, Terminal]}>` work with no compiled
165
+ * `schema.rastack.json` anywhere.
166
+ */
167
+ function toManifest(input) {
168
+ if (input === null || input === undefined)
169
+ return undefined;
170
+ if (typeof input === "string")
171
+ return JSON.parse(input);
172
+ if (Array.isArray(input) || isResourceValue(input)) {
173
+ return manifestFromResources(input);
174
+ }
175
+ const record = input;
176
+ // A compiled manifest passes through untouched.
177
+ if (Array.isArray(record.resources))
178
+ return input;
179
+ // A module namespace of resource() exports builds one.
180
+ const values = Object.values(record).filter(isResourceValue);
181
+ if (values.length)
182
+ return manifestFromResources(values);
183
+ return input;
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Option normalisers — shared with the compiler (`compile/analyze.ts`), so
187
+ // the runtime and compiled manifests can never disagree about a resource.
188
+ // ---------------------------------------------------------------------------
189
+ /**
190
+ * Attach a normalised `transitions` block to a resource and derive what the
191
+ * machine implies about its state field: the `states` become the field's
192
+ * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
193
+ * `initial` becomes its default when the field doesn't declare one.
194
+ */
195
+ function applyTransitions(resource, raw) {
196
+ const transitions = (0, transitions_1.normalizeTransitions)(raw);
197
+ if (!transitions)
198
+ return;
199
+ resource.transitions = transitions;
200
+ const stateField = resource.fields.find((f) => f.name === transitions.field);
201
+ if (stateField && stateField.type !== "fk") {
202
+ stateField.options = [...transitions.states];
203
+ if (stateField.default === undefined && transitions.initial !== undefined) {
204
+ stateField.default = transitions.initial;
205
+ }
206
+ }
207
+ }
208
+ function normaliseSync(sync) {
209
+ if (!sync || typeof sync !== "object")
210
+ return undefined;
211
+ const mode = sync.mode === "local" ? "local" : "server";
212
+ if (mode === "server" && !sync.confirmed) {
213
+ // A bare server resource carries no sync semantics.
214
+ return undefined;
215
+ }
216
+ const model = { mode };
217
+ if (typeof sync.scope === "string")
218
+ model.scope = sync.scope;
219
+ if (Array.isArray(sync.confirmed))
220
+ model.confirmed = sync.confirmed;
221
+ if (sync.load === "lazy")
222
+ model.load = "lazy";
223
+ if (sync.clientIds === true)
224
+ model.clientIds = true;
225
+ return model;
226
+ }
227
+ function normaliseAccess(access) {
228
+ if (!access || typeof access !== "object")
229
+ return undefined;
230
+ const model = {};
231
+ if (typeof access.ownerField === "string")
232
+ model.ownerField = access.ownerField;
233
+ if (access.scope === "owner" || access.scope === "shared") {
234
+ model.scope = access.scope;
235
+ }
236
+ if (Array.isArray(access.adminGroups)) {
237
+ model.adminGroups = access.adminGroups.filter((g) => typeof g === "string");
238
+ }
239
+ return Object.keys(model).length ? model : undefined;
240
+ }
241
+ function normaliseAdmin(admin) {
242
+ if (admin === false)
243
+ return false;
244
+ if (!admin || typeof admin !== "object")
245
+ return undefined;
246
+ const model = {};
247
+ if (Array.isArray(admin.listDisplay))
248
+ model.listDisplay = admin.listDisplay;
249
+ return model;
250
+ }