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.
package/hooks/entity.ts CHANGED
@@ -27,6 +27,7 @@ import type {
27
27
  TransitionsModel,
28
28
  } from "../src/compile/model";
29
29
  import type { ResourceType, RowOf } from "../src/define";
30
+ import { isResourceValue, resourceModel } from "../src/define/manifest";
30
31
  import type { EntityTypes } from "./registry";
31
32
  import { normalizeTransitions } from "../src/validate/transitions";
32
33
 
@@ -46,7 +47,9 @@ export type EntityRef<TRow = any> =
46
47
  * The row type an entity reference implies — every reference form infers:
47
48
  * classes carry it (`InstanceType`), `resource()` values derive it from their
48
49
  * fields ({@link RowOf}), and `"app.model"` strings resolve through the
49
- * {@link EntityTypes} registry `rastack compile` emits (`rastack-env.d.ts`).
50
+ * {@link EntityTypes} registry — populated by a one-line hand-written
51
+ * `EntityTypesOf<…>` declaration, or by the `rastack-env.d.ts` that
52
+ * `rastack compile` emits. No path involves generated runtime code.
50
53
  */
51
54
  export type EntityRow<E> =
52
55
  E extends EntityClass<infer R>
@@ -116,7 +119,10 @@ export function resolveEntityRef(
116
119
  manifest?: Manifest,
117
120
  ): ResolvedEntity {
118
121
  // `{ app, model }` — a `resource()` value, an EntityDescriptor, or a plain
119
- // object. The manifest enriches it (fields, transitions) when available.
122
+ // object. The manifest enriches it (fields, transitions) when available;
123
+ // a `resource()` value that a manifest doesn't cover is **its own
124
+ // manifest** — the definition carries its fields/options at runtime, so no
125
+ // compile step (and no provider config) is needed for the DSL path.
120
126
  if (typeof ref === "object" && ref !== null) {
121
127
  const { app, model } = ref as { app?: unknown; model?: unknown };
122
128
  if (typeof app !== "string" || typeof model !== "string") {
@@ -124,9 +130,9 @@ export function resolveEntityRef(
124
130
  "useData/useForm: an object entity reference needs string `app` and `model` properties",
125
131
  );
126
132
  }
127
- const resource = manifest?.resources.find(
128
- (r) => r.app === app && r.model === model,
129
- );
133
+ const resource =
134
+ manifest?.resources.find((r) => r.app === app && r.model === model) ??
135
+ (isResourceValue(ref) ? resourceModel(ref) : undefined);
130
136
  const opts = (ref as { options?: { transitions?: unknown } }).options;
131
137
  return resolved(app, model, resource, opts?.transitions);
132
138
  }
@@ -51,11 +51,8 @@ import {
51
51
  import { useRastackManifest } from "../manifest";
52
52
  import type { EntityTypes, RegisteredEntityKey } from "../registry";
53
53
  import { writeSchema } from "../../src/compile/openapi";
54
- import type {
55
- FieldModel,
56
- Manifest,
57
- ResourceModel,
58
- } from "../../src/compile/model";
54
+ import type { FieldModel, ResourceModel } from "../../src/compile/model";
55
+ import { toManifest, type ManifestInput } from "../../src/define/manifest";
59
56
  import {
60
57
  allowedTransitions,
61
58
  applyTransition,
@@ -71,8 +68,11 @@ export interface UseEntityFormOptions<TRow> {
71
68
  /** `"put"` to save with PUT; default is POST on create / PATCH on update. */
72
69
  updateType?: FormUpdate;
73
70
  onSuccess?: (row: TRow) => void;
74
- /** Manifest override — tests, or callers outside `<RAStackProvider>`. */
75
- manifest?: Manifest;
71
+ /**
72
+ * Manifest override — tests, or callers outside `<RAStackProvider>`.
73
+ * Accepts a compiled manifest or `resource()` definitions (array/module).
74
+ */
75
+ manifest?: ManifestInput;
76
76
  }
77
77
 
78
78
  /** One transition available from the record's current state. */
@@ -144,7 +144,11 @@ export function useEntityForm<TRow = any, TName extends string = string>(
144
144
  options: UseEntityFormOptions<TRow> = {},
145
145
  ): EntityFormResult<TRow, TName> {
146
146
  const contextManifest = useRastackManifest();
147
- const manifest = options.manifest ?? contextManifest;
147
+ const optionManifest = useMemo(
148
+ () => toManifest(options.manifest),
149
+ [options.manifest],
150
+ );
151
+ const manifest = optionManifest ?? contextManifest;
148
152
  const resolvedEntity = useMemo(
149
153
  () => resolveEntityRef(entity, manifest),
150
154
  [entity, manifest],
package/hooks/manifest.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
- * The compiled manifest (`schema.rastack.json`) as React context — what lets
3
- * `useData(Airport)` / `useForm(Flight)` resolve a plain TypeScript type to
4
- * its resource at runtime with no generated code in between.
2
+ * The manifest as React context — what lets `useData(Airport)` /
3
+ * `useForm(Flight)` resolve a plain TypeScript type to its resource at
4
+ * runtime with no generated code in between.
5
5
  *
6
6
  * `<RAStackProvider manifest={…}>` supplies it (in every mode — the manifest
7
7
  * is required for the WASM engine anyway, and in `remote` mode it is the only
8
- * piece of compiler output the client needs). Hooks read it with
8
+ * schema the client needs). It accepts the compiled `schema.rastack.json`
9
+ * (object or JSON string) **or the `resource()` definitions themselves** — an
10
+ * array, or a whole module namespace — in which case the manifest is built at
11
+ * runtime and no compile step is involved. Hooks read it with
9
12
  * {@link useRastackManifest}; tests and one-off callers can bypass the context
10
13
  * by passing `manifest` directly in a hook's options.
11
14
  */
@@ -13,29 +16,62 @@
13
16
  import { createContext, createElement, useContext, useMemo } from "react";
14
17
  import type { ReactNode } from "react";
15
18
  import type { Manifest } from "../src/compile/model";
19
+ import { toManifest, type ManifestInput } from "../src/define/manifest";
16
20
 
17
21
  export const RastackManifestContext = createContext<Manifest | undefined>(
18
22
  undefined,
19
23
  );
20
24
 
21
25
  export interface RastackManifestProviderProps {
22
- /** `schema.rastack.json` — parsed object or JSON string. */
23
- manifest?: Manifest | object | string;
26
+ /**
27
+ * `schema.rastack.json` (parsed object or JSON string), or `resource()`
28
+ * definitions (an array or a module namespace) to build it from at runtime.
29
+ */
30
+ manifest?: ManifestInput;
24
31
  children?: ReactNode;
25
32
  }
26
33
 
27
- /** Provide the compiled manifest to every `useData`/`useForm` beneath it. */
34
+ /** Provide the manifest to every `useData`/`useForm` beneath it. */
28
35
  export function RastackManifestProvider(props: RastackManifestProviderProps) {
29
36
  const { manifest, children } = props;
30
- const value = useMemo<Manifest | undefined>(() => {
31
- if (manifest === undefined || manifest === null) return undefined;
32
- if (typeof manifest === "string") return JSON.parse(manifest) as Manifest;
33
- return manifest as Manifest;
34
- }, [manifest]);
37
+ const value = useMemo<Manifest | undefined>(
38
+ () => toManifest(manifest) ?? getGlobalManifest(),
39
+ [manifest],
40
+ );
35
41
  return createElement(RastackManifestContext.Provider, { value }, children);
36
42
  }
37
43
 
38
- /** The compiled manifest, when a provider supplies one. */
44
+ /**
45
+ * The manifest in effect: the nearest provider's, else the one the dev-server
46
+ * plugin registered globally (see {@link getGlobalManifest}) — which is what
47
+ * lets an app pass **no manifest anywhere**.
48
+ */
39
49
  export function useRastackManifest(): Manifest | undefined {
40
- return useContext(RastackManifestContext);
50
+ return useContext(RastackManifestContext) ?? getGlobalManifest();
51
+ }
52
+
53
+ // -- the auto-registered manifest -------------------------------------------
54
+
55
+ /**
56
+ * The global the `rastack/plugin` dev-server plugin assigns before the app
57
+ * boots (its injected register module runs first): the manifest compiled
58
+ * from your TypeScript entities, refreshed on every save. With it in place,
59
+ * `<RAStackProvider mode="local">` needs no `manifest` prop and `useData` /
60
+ * `useForm` resolve entities with zero schema wiring in app code — the
61
+ * types are the only thing you author.
62
+ */
63
+ export const GLOBAL_MANIFEST_KEY = "__RASTACK_MANIFEST__";
64
+
65
+ let cachedRaw: unknown;
66
+ let cachedManifest: Manifest | undefined;
67
+
68
+ /** The plugin-registered manifest, normalised (and cached per assignment). */
69
+ export function getGlobalManifest(): Manifest | undefined {
70
+ const raw = (globalThis as Record<string, unknown>)[GLOBAL_MANIFEST_KEY];
71
+ if (raw === undefined || raw === null) return undefined;
72
+ if (raw !== cachedRaw) {
73
+ cachedRaw = raw;
74
+ cachedManifest = toManifest(raw as ManifestInput);
75
+ }
76
+ return cachedManifest;
41
77
  }
package/hooks/registry.ts CHANGED
@@ -7,8 +7,27 @@
7
7
  *
8
8
  * TypeScript can't conjure an interface out of a string on its own, so the
9
9
  * registry maps manifest keys (`"app.model"`) to your own types via ordinary
10
- * declaration merging. **You don't write it by hand**: `rastack compile`
11
- * emits `rastack-env.d.ts` alongside the manifest —
10
+ * declaration merging. Note that classes and `resource()` values never need
11
+ * it — they infer on their own; the registry only serves the string form.
12
+ * Two ways to populate it, **neither generating runtime code**:
13
+ *
14
+ * 1. **Hand-written, no compile step** — point `EntityTypesOf` at your
15
+ * resources module once; it derives every `"app.model"` key and row type
16
+ * from the definitions and tracks them forever:
17
+ *
18
+ * ```ts
19
+ * // rastack-env.d.ts — written once by you, never regenerated
20
+ * import type { EntityTypesOf } from "rastack/define";
21
+ * declare module "rastack/hooks/registry" {
22
+ * interface EntityTypes
23
+ * extends EntityTypesOf<typeof import("./resources/airports")> {}
24
+ * }
25
+ * ```
26
+ *
27
+ * 2. **Emitted by `rastack compile`** as `rastack-env.d.ts` beside the
28
+ * manifest — one entry per class/interface entity in the compiled graph
29
+ * (which have no runtime value for `EntityTypesOf` to read), pointing at
30
+ * *your* declaration:
12
31
  *
13
32
  * ```ts
14
33
  * declare module "rastack/hooks/registry" {
@@ -18,12 +37,10 @@
18
37
  * }
19
38
  * ```
20
39
  *
21
- * — one entry per class/interface entity in the compiled graph, pointing at
22
- * *your* declaration (types only, no runtime code, nothing to import). With
23
- * the file in the program, `useData` / `useRecord` / `useForm` overloads
24
- * resolve `"app.model"` arguments through {@link EntityTypes}; unregistered
25
- * strings fall back to `any` (or an explicit `useData<ITerminal>(…)`
26
- * argument, which always still works).
40
+ * With either file in the program, `useData` / `useRecord` / `useForm`
41
+ * overloads resolve `"app.model"` arguments through {@link EntityTypes};
42
+ * unregistered strings fall back to `any` (or an explicit
43
+ * `useData<ITerminal>(…)` argument, which always still works).
27
44
  */
28
45
 
29
46
  /** `"app.model"` → row type. Populated by declaration merging — see above. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.50",
3
+ "version": "0.0.51",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
package/plugin.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * `rastack/plugin` — the dev-server schema compiler. Add it to the bundler
3
+ * you already run (Vite/Rollup: `rastackPlugin()`; anything else:
4
+ * `startRastackWatcher()`) and class/interface entities compile to the
5
+ * manifest on every save — no `rastack compile` command in the loop.
6
+ * Node-only: import from build config, never from app code.
7
+ */
8
+ export * from "./src/plugin";
@@ -33,6 +33,8 @@ import {
33
33
  isAccessRevoked,
34
34
  purgeIdentityCache,
35
35
  } from "../cache";
36
+ import { toManifest } from "../src/define/manifest";
37
+ import { getGlobalManifest } from "../hooks/manifest";
36
38
 
37
39
  const RastackContext = createContext<RastackClient | null>(null);
38
40
 
@@ -57,6 +59,16 @@ export function RAStackProvider({
57
59
  const persistRef = useRef<(() => void) & { flush: () => void }>();
58
60
  const cacheStorageRef = useRef<CacheStorage | null>(null);
59
61
 
62
+ // The `manifest` prop is optional: when the rastack dev-server plugin is
63
+ // in the bundler it registers the compiled schema globally before the app
64
+ // boots, so `<RAStackProvider mode="local">` needs no schema wiring at
65
+ // all. An explicit prop (schema.rastack.json object/string, or resource()
66
+ // definitions — array or module namespace) always wins.
67
+ const manifest = useMemo(
68
+ () => toManifest(config.manifest) ?? getGlobalManifest(),
69
+ [config.manifest],
70
+ );
71
+
60
72
  const setStatusSafe = (next: RadStatus) => {
61
73
  setStatus(next);
62
74
  config.onStatusChange?.(next);
@@ -110,15 +122,17 @@ export function RAStackProvider({
110
122
  }
111
123
 
112
124
  // -- WASM modes (local / s3) --
113
- if (!config.manifest) {
114
- throw new Error("RAStackProvider: `manifest` is required in local/s3 mode.");
125
+ if (!manifest) {
126
+ throw new Error(
127
+ "RAStackProvider: no manifest found for local/s3 mode — add rastackPlugin() to your bundler (rastack/plugin), or pass the `manifest` prop.",
128
+ );
115
129
  }
116
130
 
117
131
  setStatusSafe("loading");
118
132
  const loader = config.loadEngine ?? defaultEngineLoader;
119
133
  const mod = await loader();
120
134
  if (cancelled) return;
121
- const engine = createEngine(mod, config.manifest, config.openapi);
135
+ const engine = createEngine(mod, manifest, config.openapi);
122
136
 
123
137
  // Run the in-browser engine as the signed-in identity so owner stamping
124
138
  // and row-level security match the server exactly.
@@ -140,7 +154,7 @@ export function RAStackProvider({
140
154
  await engine.importWarehouse(existing);
141
155
  } else {
142
156
  const base = config.warehouseBaseUrl ?? "/data/warehouse/";
143
- const resources = manifestResources(config.manifest);
157
+ const resources = manifestResources(manifest);
144
158
  if (config.cache === false) {
145
159
  await seedFromIceberg(engine, base, resources);
146
160
  } else {
@@ -230,7 +244,7 @@ export function RAStackProvider({
230
244
 
231
245
  return (
232
246
  <RastackContext.Provider value={client}>
233
- <RastackManifestProvider manifest={config.manifest}>
247
+ <RastackManifestProvider manifest={manifest}>
234
248
  {children}
235
249
  </RastackManifestProvider>
236
250
  </RastackContext.Provider>
package/provider/types.ts CHANGED
@@ -98,12 +98,19 @@ export interface RAStackProviderConfig {
98
98
 
99
99
  // -- all modes --
100
100
  /**
101
- * `schema.rastack.json` — object or JSON string. Required for WASM modes
102
- * (it drives the in-browser engine), and recommended in `remote` mode too:
103
- * it is what lets the generic `useData(Entity)` / `useForm(Entity)` hooks
104
- * resolve a plain TypeScript type to its resource at runtime.
101
+ * The schema — **usually omitted**: with the `rastack/plugin` dev-server
102
+ * plugin in the bundler, the manifest compiled from your entities is
103
+ * registered globally before the app boots and the provider picks it up
104
+ * automatically. Pass it explicitly to override: the compiled
105
+ * `schema.rastack.json` (object or JSON string), the
106
+ * `virtual:rastack-manifest` module, or `resource()` definitions (an array
107
+ * or a module namespace), from which the provider builds the identical
108
+ * manifest at runtime. One of the two (plugin or prop) is required for
109
+ * WASM modes (the manifest drives the in-browser engine); it is also what
110
+ * lets `useData(Entity)` / `useForm(Entity)` resolve a plain TypeScript
111
+ * type to its resource at runtime.
105
112
  */
106
- manifest?: object | string;
113
+ manifest?: object | readonly object[] | string;
107
114
  /** `openapi.json` — object or JSON string. Optional (`/api/schema/`). */
108
115
  openapi?: object | string;
109
116
  /**
@@ -1,18 +1,26 @@
1
1
  import * as path from "path";
2
2
  import * as ts from "typescript";
3
3
  import {
4
- AccessModel,
5
- AdminModel,
6
4
  FieldModel,
7
5
  RelationModel,
8
6
  ResourceModel,
9
7
  ScalarType,
10
- SyncModel,
11
8
  } from "./model";
12
9
  import { resourceSourceFiles } from "./program";
13
- import { normalizeTransitions } from "../validate/transitions";
10
+ import {
11
+ applyTransitions,
12
+ normaliseAccess,
13
+ normaliseAdmin,
14
+ normaliseSync,
15
+ } from "../define/manifest";
14
16
  import type { EntityTypeExport } from "./entities";
15
17
 
18
+ // The option normalisers live beside the runtime manifest builder
19
+ // (`define/manifest.ts`) and are shared with it, so a manifest built from
20
+ // runtime `resource()` values and one compiled from the same source can
21
+ // never disagree. Re-exported here for existing importers.
22
+ export { applyTransitions } from "../define/manifest";
23
+
16
24
  /**
17
25
  * Statically analyse resource definitions and produce the canonical model.
18
26
  *
@@ -147,26 +155,6 @@ function analyzeResourceCall(
147
155
  return resource;
148
156
  }
149
157
 
150
- /**
151
- * Attach a normalised `transitions` block to a resource and derive what the
152
- * machine implies about its state field: the `states` become the field's
153
- * allowed-value set (`enum` in OpenAPI, the `member` gate in validation), and
154
- * `initial` becomes its default when the field doesn't declare one.
155
- */
156
- export function applyTransitions(resource: ResourceModel, raw: unknown): void {
157
- const transitions = normalizeTransitions(raw);
158
- if (!transitions) return;
159
- resource.transitions = transitions;
160
-
161
- const stateField = resource.fields.find((f) => f.name === transitions.field);
162
- if (stateField && stateField.type !== "fk") {
163
- stateField.options = [...transitions.states];
164
- if (stateField.default === undefined && transitions.initial !== undefined) {
165
- stateField.default = transitions.initial;
166
- }
167
- }
168
- }
169
-
170
158
  /**
171
159
  * Resolve a single field from its initializer. FK detection is type-driven:
172
160
  * the initializer's *type* is inspected, and a `Ref` type yields a foreign key.
@@ -280,41 +268,3 @@ export function literalToValue(node: ts.Expression | undefined): any {
280
268
  }
281
269
  return undefined;
282
270
  }
283
-
284
- function normaliseSync(sync: any): SyncModel | undefined {
285
- if (!sync || typeof sync !== "object") return undefined;
286
- const mode = sync.mode === "local" ? "local" : "server";
287
- if (mode === "server" && !sync.confirmed) {
288
- // A bare server resource carries no sync semantics.
289
- return undefined;
290
- }
291
- const model: SyncModel = { mode };
292
- if (typeof sync.scope === "string") model.scope = sync.scope;
293
- if (Array.isArray(sync.confirmed)) model.confirmed = sync.confirmed;
294
- if (sync.load === "lazy") model.load = "lazy";
295
- if (sync.clientIds === true) model.clientIds = true;
296
- return model;
297
- }
298
-
299
- function normaliseAccess(access: any): AccessModel | undefined {
300
- if (!access || typeof access !== "object") return undefined;
301
- const model: AccessModel = {};
302
- if (typeof access.ownerField === "string") model.ownerField = access.ownerField;
303
- if (access.scope === "owner" || access.scope === "shared") {
304
- model.scope = access.scope;
305
- }
306
- if (Array.isArray(access.adminGroups)) {
307
- model.adminGroups = access.adminGroups.filter(
308
- (g: unknown) => typeof g === "string",
309
- );
310
- }
311
- return Object.keys(model).length ? model : undefined;
312
- }
313
-
314
- function normaliseAdmin(admin: any): AdminModel | false | undefined {
315
- if (admin === false) return false;
316
- if (!admin || typeof admin !== "object") return undefined;
317
- const model: AdminModel = {};
318
- if (Array.isArray(admin.listDisplay)) model.listDisplay = admin.listDisplay;
319
- return model;
320
- }
@@ -23,14 +23,32 @@
23
23
  * ```
24
24
  */
25
25
 
26
+ /**
27
+ * Every constraint a scalar builder may be handed, merged — the shape the
28
+ * runtime value carries so a manifest can be assembled without the compiler.
29
+ */
30
+ export interface ScalarFieldOptions {
31
+ maxLength?: number;
32
+ unique?: boolean;
33
+ primaryKey?: boolean;
34
+ default?: unknown;
35
+ null?: boolean;
36
+ autoNow?: boolean;
37
+ autoNowAdd?: boolean;
38
+ }
39
+
26
40
  /**
27
41
  * A scalar field. The literal `__rastackScalar` type is what the compiler
28
42
  * reads; `Nullable` phantom-carries the `null: true` option so {@link RowOf}
29
- * can add `| null` to the inferred row type.
43
+ * can add `| null` to the inferred row type. The authored options ride along
44
+ * on the runtime value too, so `manifestFromResources()` can build the same
45
+ * manifest `rastack compile` emits — with no compile step at all.
30
46
  */
31
47
  export interface Scalar<T extends string, Nullable extends boolean = boolean> {
32
48
  readonly __rastackScalar: T;
33
49
  readonly __rastackNullable?: Nullable;
50
+ /** The constraint options as authored — carried at runtime. */
51
+ readonly options?: ScalarFieldOptions;
34
52
  }
35
53
 
36
54
  /** Whether an options literal declares `null: true` (as a type). */
@@ -38,12 +56,16 @@ type NullOf<O> = O extends { null: true } ? true : false;
38
56
 
39
57
  /**
40
58
  * A foreign-key field. `App`/`Model` are carried as string-literal type
41
- * arguments so the compiler can recover the relation target purely from types.
59
+ * arguments so the compiler can recover the relation target purely from
60
+ * types; the target thunk is *also* kept on the runtime value so
61
+ * `manifestFromResources()` can recover the relation with no compiler.
42
62
  */
43
63
  export interface Ref<App extends string, Model extends string> {
44
64
  readonly __rastackRef: true;
45
65
  readonly __app: App;
46
66
  readonly __model: Model;
67
+ /** The authored `() => Resource` thunk — carried at runtime. */
68
+ readonly target?: () => ResourceType<App, Model, any>;
47
69
  }
48
70
 
49
71
  export interface StringOptions {
@@ -73,51 +95,66 @@ export interface DateOptions {
73
95
  }
74
96
 
75
97
  /**
76
- * Field builders. Each returns a phantom value whose *type* encodes the field
77
- * kind; the runtime object is never used at compile time (the compiler analyses
78
- * the source, it does not execute it) but is real so the DSL is also usable at
79
- * runtime if a consumer wants it.
98
+ * Field builders. Each returns a value whose *type* encodes the field kind
99
+ * (what the compiler reads — it analyses the source, it does not execute it)
100
+ * and whose runtime shape keeps the authored options/target, so the same
101
+ * definition doubles as its own manifest via `manifestFromResources()`.
80
102
  */
81
103
  export const s = {
82
104
  string<const O extends StringOptions = StringOptions>(
83
- _opts?: O,
105
+ opts?: O,
84
106
  ): Scalar<"string", NullOf<O>> {
85
- return { __rastackScalar: "string" } as Scalar<"string", NullOf<O>>;
107
+ return { __rastackScalar: "string", options: opts } as Scalar<
108
+ "string",
109
+ NullOf<O>
110
+ >;
86
111
  },
87
112
  int<const O extends NumberOptions = NumberOptions>(
88
- _opts?: O,
113
+ opts?: O,
89
114
  ): Scalar<"int", NullOf<O>> {
90
- return { __rastackScalar: "int" } as Scalar<"int", NullOf<O>>;
115
+ return { __rastackScalar: "int", options: opts } as Scalar<"int", NullOf<O>>;
91
116
  },
92
117
  float<const O extends NumberOptions = NumberOptions>(
93
- _opts?: O,
118
+ opts?: O,
94
119
  ): Scalar<"float", NullOf<O>> {
95
- return { __rastackScalar: "float" } as Scalar<"float", NullOf<O>>;
120
+ return { __rastackScalar: "float", options: opts } as Scalar<
121
+ "float",
122
+ NullOf<O>
123
+ >;
96
124
  },
97
125
  bool<const O extends BoolOptions = BoolOptions>(
98
- _opts?: O,
126
+ opts?: O,
99
127
  ): Scalar<"bool", NullOf<O>> {
100
- return { __rastackScalar: "bool" } as Scalar<"bool", NullOf<O>>;
128
+ return { __rastackScalar: "bool", options: opts } as Scalar<
129
+ "bool",
130
+ NullOf<O>
131
+ >;
101
132
  },
102
133
  datetime<const O extends DateOptions = DateOptions>(
103
- _opts?: O,
134
+ opts?: O,
104
135
  ): Scalar<"datetime", NullOf<O>> {
105
- return { __rastackScalar: "datetime" } as Scalar<"datetime", NullOf<O>>;
136
+ return { __rastackScalar: "datetime", options: opts } as Scalar<
137
+ "datetime",
138
+ NullOf<O>
139
+ >;
106
140
  },
107
141
  uuid<const O extends StringOptions = StringOptions>(
108
- _opts?: O,
142
+ opts?: O,
109
143
  ): Scalar<"uuid", NullOf<O>> {
110
- return { __rastackScalar: "uuid" } as Scalar<"uuid", NullOf<O>>;
144
+ return { __rastackScalar: "uuid", options: opts } as Scalar<
145
+ "uuid",
146
+ NullOf<O>
147
+ >;
111
148
  },
112
149
  /**
113
150
  * A foreign key. The target is `() => SomeResource`; the compiler resolves
114
- * the relation from the *type* `Ref<App, Model>` this returns — it does not
115
- * need to run the thunk.
151
+ * the relation from the *type* `Ref<App, Model>` this returns, and the
152
+ * runtime keeps the thunk so `manifestFromResources()` can resolve it too.
116
153
  */
117
154
  ref<A extends string, M extends string>(
118
- _thunk: () => ResourceType<A, M, any>,
155
+ thunk: () => ResourceType<A, M, any>,
119
156
  ): Ref<A, M> {
120
- return { __rastackRef: true } as Ref<A, M>;
157
+ return { __rastackRef: true, target: thunk } as Ref<A, M>;
121
158
  },
122
159
  };
123
160
 
@@ -310,3 +347,42 @@ export type RowOf<R> =
310
347
  R extends ResourceType<string, string, infer F, ResourceOptions>
311
348
  ? { id: number | string } & { [K in keyof F]: FieldValueOf<F[K]> }
312
349
  : never;
350
+
351
+ /** The `"app.model"` registry key a `resource()` definition implies. */
352
+ export type EntityKeyOf<R> =
353
+ R extends ResourceType<infer A, infer M, any, ResourceOptions>
354
+ ? `${A}.${M}`
355
+ : never;
356
+
357
+ /**
358
+ * The `EntityTypes` registry entries a module of `resource()` definitions
359
+ * implies — the *hand-written* alternative to the `rastack-env.d.ts` that
360
+ * `rastack compile` emits. Point it at your resources module once and the
361
+ * registry tracks it forever, with no compile step in the loop:
362
+ *
363
+ * ```ts
364
+ * // rastack-env.d.ts — written once by you, never regenerated
365
+ * import type { EntityTypesOf } from "rastack/define";
366
+ * declare module "rastack/hooks/registry" {
367
+ * interface EntityTypes
368
+ * extends EntityTypesOf<typeof import("./resources/airports")> {}
369
+ * }
370
+ * ```
371
+ *
372
+ * Non-resource exports are dropped, so `typeof import("…")` over a whole
373
+ * module is safe. (Class entities carry their own row type — they never need
374
+ * the registry; interfaces still register via the compile-emitted file.)
375
+ */
376
+ export type EntityTypesOf<M> = {
377
+ [K in keyof M as EntityKeyOf<M[K]>]: RowOf<M[K]>;
378
+ };
379
+
380
+ // The runtime manifest builder — a resource() definition is its own manifest.
381
+ export {
382
+ isResourceValue,
383
+ manifestFromResources,
384
+ resourceModel,
385
+ toManifest,
386
+ } from "./manifest";
387
+ export type { AnyResourceValue, ManifestInput } from "./manifest";
388
+ export type { Manifest, ResourceModel } from "../compile/model";