rastack 0.0.49 → 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.
Files changed (68) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/components/auto-form/AutoForm.tsx +13 -0
  3. package/components/auto-form/use-auto-form.ts +28 -0
  4. package/components/types.ts +8 -0
  5. package/dist/admin.js +13 -13
  6. package/dist/compile/analyze.d.ts +20 -0
  7. package/dist/compile/analyze.js +60 -49
  8. package/dist/compile/entities.d.ts +19 -0
  9. package/dist/compile/entities.js +87 -13
  10. package/dist/compile/index.d.ts +23 -4
  11. package/dist/compile/index.js +86 -7
  12. package/dist/compile/model.d.ts +38 -0
  13. package/dist/compile/openapi.d.ts +9 -0
  14. package/dist/compile/openapi.js +14 -0
  15. package/dist/define/index.d.ts +162 -21
  16. package/dist/define/index.js +28 -22
  17. package/dist/define/manifest.d.ts +64 -0
  18. package/dist/define/manifest.js +250 -0
  19. package/dist/import/tabular.d.ts +8 -2
  20. package/dist/import/tabular.js +1 -1
  21. package/dist/plugin/core.d.ts +108 -0
  22. package/dist/plugin/core.js +198 -0
  23. package/dist/plugin/index.d.ts +112 -0
  24. package/dist/plugin/index.js +203 -0
  25. package/dist/rastack-import.js +4 -1
  26. package/dist/validate/adapters.js +2 -0
  27. package/dist/validate/index.d.ts +1 -0
  28. package/dist/validate/index.js +1 -0
  29. package/dist/validate/machine.d.ts +23 -2
  30. package/dist/validate/machine.js +35 -2
  31. package/dist/validate/transitions.d.ts +86 -0
  32. package/dist/validate/transitions.js +199 -0
  33. package/dist/wasm/rastack_wasm.js +1 -1
  34. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  35. package/hooks/data.ts +221 -0
  36. package/hooks/entity.ts +228 -0
  37. package/hooks/form/entity-form.ts +358 -0
  38. package/hooks/form/form.ts +8 -1
  39. package/hooks/form/index.ts +7 -1
  40. package/hooks/index.ts +4 -0
  41. package/hooks/manifest.ts +77 -0
  42. package/hooks/registry.ts +56 -0
  43. package/package.json +1 -1
  44. package/plugin.ts +8 -0
  45. package/provider/provider.tsx +26 -5
  46. package/provider/types.ts +15 -3
  47. package/src/compile/analyze.ts +74 -45
  48. package/src/compile/entities.ts +111 -11
  49. package/src/compile/index.ts +108 -11
  50. package/src/compile/model.ts +40 -0
  51. package/src/compile/openapi.ts +13 -1
  52. package/src/define/index.ts +233 -29
  53. package/src/define/manifest.ts +278 -0
  54. package/src/import/tabular.ts +9 -2
  55. package/src/plugin/core.ts +236 -0
  56. package/src/plugin/index.ts +243 -0
  57. package/src/rastack-import.ts +4 -1
  58. package/src/validate/adapters.ts +1 -0
  59. package/src/validate/index.ts +1 -0
  60. package/src/validate/machine.ts +55 -3
  61. package/src/validate/transitions.ts +232 -0
  62. package/test/components.spec.ts +22 -0
  63. package/test/plugin.spec.ts +315 -0
  64. package/test/runtime-manifest.spec.ts +309 -0
  65. package/test/transitions.spec.ts +372 -0
  66. package/test/typed-hooks.spec.ts +412 -0
  67. package/wasm/rastack_wasm.js +1 -1
  68. package/wasm/rastack_wasm_bg.wasm +0 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Entity references — how `useData(...)` / `useForm(...)` know which resource
3
+ * a plain TypeScript type is, with **no generated hooks in between**.
4
+ *
5
+ * A hook accepts any of:
6
+ *
7
+ * - **the entity class itself** — `useData(Airport)`. Classes are values, so
8
+ * the class *is* the reference; its name resolves against the manifest with
9
+ * the same normalisation the compiler applies (`IFlight` → `flight`).
10
+ * - **a `resource()` definition** — `useData(Flight)`. The DSL value carries
11
+ * `app`/`model` (and its literal-typed options, so transition names are
12
+ * typed) at runtime.
13
+ * - **an `"app.model"` (or bare `"model"`) string** — the escape hatch for
14
+ * interface entities, which have no runtime value:
15
+ * `useData<Terminal>("airports.terminal")`.
16
+ * - **any `{ app, model }` object** — e.g. a sync `EntityDescriptor`.
17
+ *
18
+ * Resolution is a pure function over the compiled manifest
19
+ * (`schema.rastack.json`), which `<RAStackProvider manifest={…}>` supplies via
20
+ * context — the type system names the entity, the manifest carries its
21
+ * schema, and the codegen'd per-entity hook layer disappears.
22
+ */
23
+
24
+ import type {
25
+ Manifest,
26
+ ResourceModel,
27
+ TransitionsModel,
28
+ } from "../src/compile/model";
29
+ import type { ResourceType, RowOf } from "../src/define";
30
+ import { isResourceValue, resourceModel } from "../src/define/manifest";
31
+ import type { EntityTypes } from "./registry";
32
+ import { normalizeTransitions } from "../src/validate/transitions";
33
+
34
+ /** A class used as an entity reference (`useData(Airport)`). */
35
+ export type EntityClass<TRow = any> = new (...args: any[]) => TRow;
36
+
37
+ /** A `resource()` definition used as an entity reference (`useData(Flight)`). */
38
+ export type EntityResource = ResourceType<string, string, any, any>;
39
+
40
+ /** Anything that can name an entity. */
41
+ export type EntityRef<TRow = any> =
42
+ | string
43
+ | EntityClass<TRow>
44
+ | { app: string; model: string };
45
+
46
+ /**
47
+ * The row type an entity reference implies — every reference form infers:
48
+ * classes carry it (`InstanceType`), `resource()` values derive it from their
49
+ * fields ({@link RowOf}), and `"app.model"` strings resolve through the
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.
53
+ */
54
+ export type EntityRow<E> =
55
+ E extends EntityClass<infer R>
56
+ ? R
57
+ : E extends EntityResource
58
+ ? RowOf<E>
59
+ : E extends keyof EntityTypes
60
+ ? EntityTypes[E]
61
+ : any;
62
+
63
+ /** A reference resolved against the manifest — everything a hook needs. */
64
+ export interface ResolvedEntity {
65
+ app: string;
66
+ model: string;
67
+ /** `"app.model"` — cache keys, sync store keys. */
68
+ key: string;
69
+ /** `/api/{app}/v1/{model}/` — the URL convention is unchanged. */
70
+ listUrl: string;
71
+ detailUrl: (id: string | number) => string;
72
+ /** The manifest resource (present whenever a manifest was available). */
73
+ resource?: ResourceModel;
74
+ /** The resource's state machine, when it declares one. */
75
+ transitions?: TransitionsModel;
76
+ }
77
+
78
+ /**
79
+ * The manifest model name a TypeScript type name maps to — the *same*
80
+ * normalisation the compiler applies (`entityNaming` in `compile/entities.ts`):
81
+ * a leading `I` prefix is stripped (`IFlight` → `Flight`) and the first letter
82
+ * lower-cased.
83
+ */
84
+ export function entityModelName(typeName: string): string {
85
+ const stripped = /^I[A-Z]/.test(typeName) ? typeName.slice(1) : typeName;
86
+ return stripped.charAt(0).toLowerCase() + stripped.slice(1);
87
+ }
88
+
89
+ function resolved(
90
+ app: string,
91
+ model: string,
92
+ resource?: ResourceModel,
93
+ fallbackTransitions?: unknown,
94
+ ): ResolvedEntity {
95
+ const listUrl = `/api/${app}/v1/${model}/`;
96
+ return {
97
+ app,
98
+ model,
99
+ key: `${app}.${model}`,
100
+ listUrl,
101
+ detailUrl: (id) => `${listUrl}${id}/`,
102
+ resource,
103
+ transitions:
104
+ resource?.transitions ?? normalizeTransitions(fallbackTransitions),
105
+ };
106
+ }
107
+
108
+ function findByModel(manifest: Manifest, model: string): ResourceModel[] {
109
+ return manifest.resources.filter((r) => r.model === model);
110
+ }
111
+
112
+ /**
113
+ * Resolve an entity reference against the manifest. Throws a descriptive
114
+ * error on an unknown or ambiguous reference — resolution failures are
115
+ * authoring mistakes and should fail loudly, not degrade into 404s.
116
+ */
117
+ export function resolveEntityRef(
118
+ ref: EntityRef,
119
+ manifest?: Manifest,
120
+ ): ResolvedEntity {
121
+ // `{ app, model }` — a `resource()` value, an EntityDescriptor, or a plain
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.
126
+ if (typeof ref === "object" && ref !== null) {
127
+ const { app, model } = ref as { app?: unknown; model?: unknown };
128
+ if (typeof app !== "string" || typeof model !== "string") {
129
+ throw new Error(
130
+ "useData/useForm: an object entity reference needs string `app` and `model` properties",
131
+ );
132
+ }
133
+ const resource =
134
+ manifest?.resources.find((r) => r.app === app && r.model === model) ??
135
+ (isResourceValue(ref) ? resourceModel(ref) : undefined);
136
+ const opts = (ref as { options?: { transitions?: unknown } }).options;
137
+ return resolved(app, model, resource, opts?.transitions);
138
+ }
139
+
140
+ // A class — the flagship form. Its name resolves like the compiler resolves
141
+ // it, and a `static transitions` block rides along as a manifest fallback.
142
+ if (typeof ref === "function") {
143
+ const name = ref.name;
144
+ if (!name) {
145
+ throw new Error("useData/useForm: anonymous classes cannot name an entity");
146
+ }
147
+ if (!manifest) {
148
+ throw new Error(
149
+ `useData/useForm: resolving the ${name} class needs the compiled manifest — pass \`manifest\` to <RAStackProvider> (or in the hook's options)`,
150
+ );
151
+ }
152
+ const model = entityModelName(name);
153
+ const matches = findByModel(manifest, model);
154
+ if (matches.length === 1) {
155
+ return resolved(
156
+ matches[0].app,
157
+ matches[0].model,
158
+ matches[0],
159
+ (ref as { transitions?: unknown }).transitions,
160
+ );
161
+ }
162
+ if (matches.length === 0) {
163
+ throw new Error(
164
+ `useData/useForm: no resource named "${model}" in the manifest — is ${name} part of the compiled resource graph (\`rastack compile\`)?`,
165
+ );
166
+ }
167
+ throw new Error(
168
+ `useData/useForm: "${model}" exists in ${matches.length} apps (${matches
169
+ .map((m) => `${m.app}.${m.model}`)
170
+ .join(", ")}) — pass the explicit "app.model" string instead`,
171
+ );
172
+ }
173
+
174
+ // A string — `"app.model"`, or a bare `"model"` resolved via the manifest.
175
+ if (typeof ref === "string" && ref) {
176
+ const dot = ref.indexOf(".");
177
+ if (dot > 0) {
178
+ const app = ref.slice(0, dot);
179
+ const model = ref.slice(dot + 1);
180
+ const resource = manifest?.resources.find(
181
+ (r) => r.app === app && r.model === model,
182
+ );
183
+ if (manifest && !resource) {
184
+ throw new Error(
185
+ `useData/useForm: no resource "${ref}" in the manifest`,
186
+ );
187
+ }
188
+ return resolved(app, model, resource);
189
+ }
190
+ if (!manifest) {
191
+ throw new Error(
192
+ `useData/useForm: resolving "${ref}" needs the compiled manifest — pass \`manifest\` to <RAStackProvider>, or use the full "app.model" form`,
193
+ );
194
+ }
195
+ const matches = findByModel(manifest, ref);
196
+ if (matches.length === 1) {
197
+ return resolved(matches[0].app, matches[0].model, matches[0]);
198
+ }
199
+ throw new Error(
200
+ matches.length === 0
201
+ ? `useData/useForm: no resource named "${ref}" in the manifest`
202
+ : `useData/useForm: "${ref}" exists in ${matches.length} apps — use the "app.model" form`,
203
+ );
204
+ }
205
+
206
+ throw new Error("useData/useForm: unsupported entity reference");
207
+ }
208
+
209
+ /** Whether a value is an entity reference (vs. legacy `useForm(props)`). */
210
+ export function isEntityRef(value: unknown): value is EntityRef {
211
+ if (typeof value === "string" || typeof value === "function") return true;
212
+ return (
213
+ !!value &&
214
+ typeof value === "object" &&
215
+ typeof (value as { app?: unknown }).app === "string" &&
216
+ typeof (value as { model?: unknown }).model === "string"
217
+ );
218
+ }
219
+
220
+ /** `snake_or_camel` → `Snake or camel` — column/action labels. */
221
+ export function humanizeName(key: string): string {
222
+ const spaced = key
223
+ .replace(/_/g, " ")
224
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
225
+ .toLowerCase()
226
+ .trim();
227
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
228
+ }
@@ -0,0 +1,358 @@
1
+ /**
2
+ * `useForm` — pass your type in, get the whole form out.
3
+ *
4
+ * ```tsx
5
+ * const form = useForm(Flight, { id });
6
+ *
7
+ * <AutoForm form={form} />
8
+ * form.transitions // [{ name: "board", to: "boarding", … }] — from the current state
9
+ * form.transition("board") // fire a declared edge: state change + `set` patches, then save
10
+ * ```
11
+ *
12
+ * The generic replacement for the generated `useUpdate{Model}Form` hooks:
13
+ * the entity reference resolves against the compiled manifest, the write
14
+ * JSON Schema is derived from the *same* `writeSchema()` the OpenAPI emitter
15
+ * uses (so the form validates exactly the contract the API enforces —
16
+ * datatypes, `maxLength`, `enum`, FK relations, the constraint state
17
+ * machine), the create/put/patch mutations are built from the URL convention,
18
+ * and — when the resource declares a `transitions` block — the form exposes
19
+ * the state machine as typed actions.
20
+ *
21
+ * `form.transition(name)` is the client half of the declarative backend
22
+ * function: it validates the edge against the record's current state, stages
23
+ * the state change plus the edge's `set` patches on top of the form values,
24
+ * and submits. The server (`rastack-api-core` — HTTP, Lambda, and in-browser
25
+ * WASM alike) re-derives and re-enforces the same edge from the same
26
+ * manifest, so an illegal jump is rejected even if a client bypasses the
27
+ * form. Transition *names* are typed: `useForm(Flight).transition(…)` only
28
+ * accepts the names `Flight` declares.
29
+ */
30
+
31
+ import { useMemo } from "react";
32
+ import { useSchemaForm } from "./form";
33
+ import type {
34
+ FormProps,
35
+ FormStructure,
36
+ FormUpdate,
37
+ FormValuesType,
38
+ } from "./interfaces";
39
+ import { useGetQuery } from "../query/fetch";
40
+ import { useUpdateQuery } from "../query/update";
41
+ import type { IGetQueryResult } from "../query/interfaces";
42
+ import {
43
+ EntityClass,
44
+ EntityRef,
45
+ EntityRow,
46
+ humanizeName,
47
+ isEntityRef,
48
+ ResolvedEntity,
49
+ resolveEntityRef,
50
+ } from "../entity";
51
+ import { useRastackManifest } from "../manifest";
52
+ import type { EntityTypes, RegisteredEntityKey } from "../registry";
53
+ import { writeSchema } from "../../src/compile/openapi";
54
+ import type { FieldModel, ResourceModel } from "../../src/compile/model";
55
+ import { toManifest, type ManifestInput } from "../../src/define/manifest";
56
+ import {
57
+ allowedTransitions,
58
+ applyTransition,
59
+ currentState,
60
+ } from "../../src/validate/transitions";
61
+ import type { TransitionNamesOf } from "../../src/define";
62
+
63
+ export interface UseEntityFormOptions<TRow> {
64
+ /** Editing an existing record: its id. Absent = a create form. */
65
+ id?: string | number;
66
+ /** Initial/override values, merged over defaults and the fetched record. */
67
+ formValues?: Partial<TRow>;
68
+ /** `"put"` to save with PUT; default is POST on create / PATCH on update. */
69
+ updateType?: FormUpdate;
70
+ onSuccess?: (row: TRow) => void;
71
+ /**
72
+ * Manifest override — tests, or callers outside `<RAStackProvider>`.
73
+ * Accepts a compiled manifest or `resource()` definitions (array/module).
74
+ */
75
+ manifest?: ManifestInput;
76
+ }
77
+
78
+ /** One transition available from the record's current state. */
79
+ export interface FormTransition {
80
+ name: string;
81
+ /** Humanised name — a ready-made button label. */
82
+ label: string;
83
+ /** The state the transition enters. */
84
+ to: string;
85
+ /** The edge's `set` patches (applied alongside the state change). */
86
+ set?: Record<string, unknown>;
87
+ }
88
+
89
+ export interface EntityFormResult<TRow, TName extends string = string>
90
+ extends FormStructure<Partial<TRow>, Record<string, unknown>> {
91
+ /** The resolved entity — urls, manifest resource, state machine. */
92
+ entity: ResolvedEntity;
93
+ /** The persisted record being edited (id mode), once fetched. */
94
+ record: TRow | undefined;
95
+ isRecordLoading: boolean;
96
+ /** The record's current machine state (resources with `transitions`). */
97
+ state: string | undefined;
98
+ /** The transitions available from the current state — render as actions. */
99
+ transitions: FormTransition[];
100
+ /** Whether a named transition may fire from the current state. */
101
+ canTransition: (name: TName) => boolean;
102
+ /**
103
+ * Fire a declared transition: guard the edge, stage `{ [field]: to, ...set }`
104
+ * over the current form values, and save. Throws on an edge the state
105
+ * machine does not allow — the same rule the engine enforces on write.
106
+ */
107
+ transition: (name: TName) => void;
108
+ }
109
+
110
+ /** Form-friendly initial values straight from the manifest resource. */
111
+ export function initialValuesFromResource(
112
+ resource: ResourceModel,
113
+ ): Record<string, unknown> {
114
+ const values: Record<string, unknown> = {};
115
+ for (const field of resource.fields) {
116
+ if (field.primaryKey) continue;
117
+ values[field.name] = initialFieldValue(field);
118
+ }
119
+ return values;
120
+ }
121
+
122
+ function initialFieldValue(field: FieldModel): unknown {
123
+ if (field.default !== undefined) return field.default;
124
+ if (field.type === "bool") return false;
125
+ return "";
126
+ }
127
+
128
+ /** The writable (non-PK) slice of a fetched record, for form seeding. */
129
+ function writableValues(
130
+ resource: ResourceModel,
131
+ record: Record<string, unknown> | undefined,
132
+ ): Record<string, unknown> {
133
+ if (!record) return {};
134
+ const values: Record<string, unknown> = {};
135
+ for (const field of resource.fields) {
136
+ if (field.primaryKey) continue;
137
+ if (record[field.name] !== undefined) values[field.name] = record[field.name];
138
+ }
139
+ return values;
140
+ }
141
+
142
+ export function useEntityForm<TRow = any, TName extends string = string>(
143
+ entity: EntityRef<TRow>,
144
+ options: UseEntityFormOptions<TRow> = {},
145
+ ): EntityFormResult<TRow, TName> {
146
+ const contextManifest = useRastackManifest();
147
+ const optionManifest = useMemo(
148
+ () => toManifest(options.manifest),
149
+ [options.manifest],
150
+ );
151
+ const manifest = optionManifest ?? contextManifest;
152
+ const resolvedEntity = useMemo(
153
+ () => resolveEntityRef(entity, manifest),
154
+ [entity, manifest],
155
+ );
156
+ const resource = resolvedEntity.resource;
157
+ if (!resource) {
158
+ throw new Error(
159
+ `useForm: the ${resolvedEntity.key} resource is not in the manifest — pass \`manifest\` to <RAStackProvider> (or in the hook's options)`,
160
+ );
161
+ }
162
+
163
+ const id = options.id !== undefined && options.id !== null ? String(options.id) : undefined;
164
+ const listQueryCacheKey = `${resolvedEntity.key}-list`;
165
+ const itemQueryCacheKey = `${resolvedEntity.key}-detail`;
166
+
167
+ // The persisted record (id mode) — seeds the form and anchors the state
168
+ // machine's "from" state.
169
+ const recordQuery: IGetQueryResult<any> = useGetQuery<
170
+ Record<string, unknown>,
171
+ any
172
+ >({
173
+ getUrl: resolvedEntity.detailUrl(id ?? ""),
174
+ queryKey: itemQueryCacheKey,
175
+ pathParameters: [],
176
+ requiredParams: [],
177
+ isAuthenticated: id !== undefined,
178
+ options: { params: {} },
179
+ });
180
+ const record = recordQuery.data as Record<string, unknown> | undefined;
181
+
182
+ // The same write schema the OpenAPI emitter publishes — derived from the
183
+ // manifest at runtime instead of imported from generated code.
184
+ const createSchema = useMemo(() => writeSchema(resource, false), [resource]);
185
+ const patchSchema = useMemo(() => writeSchema(resource, true), [resource]);
186
+
187
+ const createMutation = useUpdateQuery<any, any, any>({
188
+ postUrl: resolvedEntity.listUrl,
189
+ listQueryCacheKey,
190
+ });
191
+ const putMutation = useUpdateQuery<any, any, any>({
192
+ putUrl: (params: any) => resolvedEntity.detailUrl(params.id),
193
+ listQueryCacheKey,
194
+ itemQueryCacheKey,
195
+ });
196
+ const patchMutation = useUpdateQuery<any, any, any>({
197
+ patchUrl: (params: any) => resolvedEntity.detailUrl(params.id),
198
+ listQueryCacheKey,
199
+ itemQueryCacheKey,
200
+ });
201
+
202
+ const formValues = useMemo(
203
+ () => ({
204
+ ...initialValuesFromResource(resource),
205
+ ...writableValues(resource, record),
206
+ ...options.formValues,
207
+ }),
208
+ [resource, record, options.formValues],
209
+ );
210
+
211
+ const form = useSchemaForm({
212
+ id,
213
+ formValues: formValues as any,
214
+ updateType: options.updateType as any,
215
+ mutations: {
216
+ post: { mutation: createMutation, validation: createSchema },
217
+ put: { mutation: putMutation, validation: createSchema },
218
+ patch: { mutation: patchMutation, validation: patchSchema },
219
+ },
220
+ onSuccess: options.onSuccess as any,
221
+ });
222
+
223
+ // -- the state machine, as form actions ------------------------------------
224
+ const spec = resolvedEntity.transitions;
225
+ const state = spec
226
+ ? currentState(spec, record ?? (formValues as Record<string, unknown>))
227
+ : undefined;
228
+ const transitions: FormTransition[] = useMemo(
229
+ () =>
230
+ spec
231
+ ? allowedTransitions(spec, state).map((t) => ({
232
+ name: t.name,
233
+ label: humanizeName(t.name),
234
+ to: t.to,
235
+ set: t.set,
236
+ }))
237
+ : [],
238
+ [spec, state],
239
+ );
240
+
241
+ const canTransition = (name: TName): boolean =>
242
+ transitions.some((t) => t.name === name);
243
+
244
+ const transition = (name: TName): void => {
245
+ if (!spec) {
246
+ throw new Error(`useForm: ${resolvedEntity.key} declares no transitions`);
247
+ }
248
+ if (id === undefined) {
249
+ throw new Error(
250
+ `useForm: transitions fire on a persisted record — pass \`id\``,
251
+ );
252
+ }
253
+ const applied = applyTransition(spec, name, record ?? formValues);
254
+ if (!applied.ok) {
255
+ throw new Error(`useForm: ${applied.message}`);
256
+ }
257
+ // The transition *is* the save: current form values + the edge's patch,
258
+ // written through the normal update mutation. The engine re-checks the
259
+ // edge and re-applies `set` server-side — the client stages it so the
260
+ // optimistic UI and the persisted row agree.
261
+ const payload = {
262
+ ...(form.responseValues as Record<string, unknown>),
263
+ ...applied.patch,
264
+ };
265
+ const mutation = options.updateType === "put" ? putMutation : patchMutation;
266
+ mutation.mutate(
267
+ { params: { id }, payload },
268
+ options.onSuccess
269
+ ? { onSuccess: (row: any) => options.onSuccess!(row) }
270
+ : undefined,
271
+ );
272
+ };
273
+
274
+ return {
275
+ ...(form as FormStructure<Partial<TRow>, Record<string, unknown>>),
276
+ entity: resolvedEntity,
277
+ record: record as TRow | undefined,
278
+ isRecordLoading: id !== undefined && recordQuery.isLoading === true,
279
+ state,
280
+ transitions,
281
+ canTransition,
282
+ transition,
283
+ };
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // The public `useForm` — entity-first, legacy-props compatible
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * Pass your type to `useForm` (class, `resource()` value, or `"app.model"`),
292
+ * or the legacy explicit-props object (what generated form hooks call) — the
293
+ * argument shape picks the implementation. A call site's shape never changes
294
+ * between renders, so the dispatch is hook-order safe.
295
+ */
296
+ // A registered "app.model" string infers its rows through the EntityTypes
297
+ // registry (`rastack compile` emits rastack-env.d.ts) — no type argument.
298
+ export function useForm<K extends RegisteredEntityKey>(
299
+ entity: K,
300
+ options?: UseEntityFormOptions<EntityTypes[K]>,
301
+ ): EntityFormResult<EntityTypes[K], string>;
302
+ export function useForm<E extends EntityClass<any> | { app: string; model: string }>(
303
+ entity: E,
304
+ options?: UseEntityFormOptions<EntityRow<E>>,
305
+ ): EntityFormResult<EntityRow<E>, TransitionNamesOf<E>>;
306
+ export function useForm<TRow = any>(
307
+ entity: string,
308
+ options?: UseEntityFormOptions<TRow>,
309
+ ): EntityFormResult<TRow, string>;
310
+ // The legacy explicit-props signature — what the generated per-entity form
311
+ // hooks call (with explicit type arguments), kept verbatim for compatibility.
312
+ export function useForm<
313
+ TFormValues extends object,
314
+ TPostParams,
315
+ TPutParams,
316
+ TPatchParams,
317
+ TPostPayload,
318
+ TPutPayload,
319
+ TPatchPayload,
320
+ TPostResult,
321
+ TPutResult,
322
+ TPatchResult,
323
+ TFormUpdate extends FormUpdate,
324
+ TSourceId extends string | undefined,
325
+ >(
326
+ props: FormProps<
327
+ TFormValues,
328
+ TPostParams,
329
+ TPutParams,
330
+ TPatchParams,
331
+ TPostPayload,
332
+ TPutPayload,
333
+ TPatchPayload,
334
+ TPostResult,
335
+ TPutResult,
336
+ TPatchResult,
337
+ TFormUpdate,
338
+ TSourceId
339
+ >,
340
+ ): FormStructure<
341
+ TFormValues,
342
+ FormValuesType<
343
+ TFormUpdate,
344
+ TSourceId,
345
+ TPutParams,
346
+ TPutPayload,
347
+ TPostParams,
348
+ TPostPayload,
349
+ TPatchParams,
350
+ TPatchPayload
351
+ >
352
+ >;
353
+ export function useForm(entityOrProps: any, options?: any): any {
354
+ if (isEntityRef(entityOrProps)) {
355
+ return useEntityForm(entityOrProps, options);
356
+ }
357
+ return useSchemaForm(entityOrProps);
358
+ }
@@ -11,7 +11,14 @@ import { formikJSONSchemaValidation } from "./validate-schema";
11
11
  import { getFieldMachine, getFieldStructure } from "./structure";
12
12
  import { describeMachine, runFieldMachine } from "../../src/validate";
13
13
 
14
- export function useForm<
14
+ /**
15
+ * The schema-driven form core: Formik + AJV over an explicit `mutations`
16
+ * bundle (one mutation + write JSON Schema per HTTP method). The generic
17
+ * `useForm(Entity)` (see `./entity-form.ts`) derives that bundle from the
18
+ * manifest and delegates here, and the generated per-entity form hooks call
19
+ * it with compile-time `Deref*` schemas — one form brain either way.
20
+ */
21
+ export function useSchemaForm<
15
22
  TFormValues extends object,
16
23
  TPostParams,
17
24
  TPutParams,
@@ -1,4 +1,10 @@
1
- export { useForm } from "./form";
1
+ export { useSchemaForm } from "./form";
2
+ export { useForm, useEntityForm, initialValuesFromResource } from "./entity-form";
3
+ export type {
4
+ UseEntityFormOptions,
5
+ EntityFormResult,
6
+ FormTransition,
7
+ } from "./entity-form";
2
8
  export type {
3
9
  FormStructure,
4
10
  FormUpdateMethod,
package/hooks/index.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  export * from "./form";
2
2
  export * from "./query";
3
3
  export * from "./real-time";
4
+ export * from "./entity";
5
+ export * from "./data";
6
+ export * from "./manifest";
7
+ export * from "./registry";