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/CHANGELOG.md +2 -0
- package/dist/admin.js +13 -13
- package/dist/compile/analyze.d.ts +1 -7
- package/dist/compile/analyze.js +12 -68
- package/dist/define/index.d.ts +63 -16
- package/dist/define/index.js +27 -21
- package/dist/define/manifest.d.ts +64 -0
- package/dist/define/manifest.js +250 -0
- package/dist/plugin/core.d.ts +108 -0
- package/dist/plugin/core.js +198 -0
- package/dist/plugin/index.d.ts +112 -0
- package/dist/plugin/index.js +203 -0
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/hooks/data.ts +18 -6
- package/hooks/entity.ts +11 -5
- package/hooks/form/entity-form.ts +12 -8
- package/hooks/manifest.ts +50 -14
- package/hooks/registry.ts +25 -8
- package/package.json +1 -1
- package/plugin.ts +8 -0
- package/provider/provider.tsx +19 -5
- package/provider/types.ts +12 -5
- package/src/compile/analyze.ts +12 -62
- package/src/define/index.ts +98 -22
- package/src/define/manifest.ts +278 -0
- package/src/plugin/core.ts +236 -0
- package/src/plugin/index.ts +243 -0
- package/test/plugin.spec.ts +315 -0
- package/test/runtime-manifest.spec.ts +309 -0
- package/test/typed-hooks.spec.ts +7 -2
- package/wasm/rastack_wasm_bg.wasm +0 -0
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
/**
|
|
75
|
-
|
|
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
|
|
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
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
/**
|
|
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.
|
|
11
|
-
*
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
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";
|
package/provider/provider.tsx
CHANGED
|
@@ -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 (!
|
|
114
|
-
throw new Error(
|
|
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,
|
|
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(
|
|
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={
|
|
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
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
/**
|
package/src/compile/analyze.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
}
|
package/src/define/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
-
|
|
105
|
+
opts?: O,
|
|
84
106
|
): Scalar<"string", NullOf<O>> {
|
|
85
|
-
return { __rastackScalar: "string" } as Scalar<
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
opts?: O,
|
|
94
119
|
): Scalar<"float", NullOf<O>> {
|
|
95
|
-
return { __rastackScalar: "float" } as Scalar<
|
|
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
|
-
|
|
126
|
+
opts?: O,
|
|
99
127
|
): Scalar<"bool", NullOf<O>> {
|
|
100
|
-
return { __rastackScalar: "bool" } as Scalar<
|
|
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
|
-
|
|
134
|
+
opts?: O,
|
|
104
135
|
): Scalar<"datetime", NullOf<O>> {
|
|
105
|
-
return { __rastackScalar: "datetime" } as Scalar<
|
|
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
|
-
|
|
142
|
+
opts?: O,
|
|
109
143
|
): Scalar<"uuid", NullOf<O>> {
|
|
110
|
-
return { __rastackScalar: "uuid" } as Scalar<
|
|
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
|
|
115
|
-
*
|
|
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
|
-
|
|
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";
|