zerodrift 1.0.0

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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +358 -0
  3. package/dist/core/BaseModel.d.ts +76 -0
  4. package/dist/core/BaseModel.js +505 -0
  5. package/dist/core/BaseSSEConnection.d.ts +31 -0
  6. package/dist/core/BaseSSEConnection.js +91 -0
  7. package/dist/core/BatchModelLoader.d.ts +27 -0
  8. package/dist/core/BatchModelLoader.js +70 -0
  9. package/dist/core/CompoundIndexFetcher.d.ts +46 -0
  10. package/dist/core/CompoundIndexFetcher.js +177 -0
  11. package/dist/core/Database.d.ts +303 -0
  12. package/dist/core/Database.js +837 -0
  13. package/dist/core/LazyCollection.d.ts +168 -0
  14. package/dist/core/LazyCollection.js +403 -0
  15. package/dist/core/LazyOwnedCollection.d.ts +35 -0
  16. package/dist/core/LazyOwnedCollection.js +66 -0
  17. package/dist/core/MemoryAdapter.d.ts +67 -0
  18. package/dist/core/MemoryAdapter.js +243 -0
  19. package/dist/core/ModelRegistry.d.ts +64 -0
  20. package/dist/core/ModelRegistry.js +217 -0
  21. package/dist/core/ModelStream.d.ts +33 -0
  22. package/dist/core/ModelStream.js +68 -0
  23. package/dist/core/ObjectPool.d.ts +113 -0
  24. package/dist/core/ObjectPool.js +339 -0
  25. package/dist/core/Store.d.ts +40 -0
  26. package/dist/core/Store.js +73 -0
  27. package/dist/core/StoreManager.d.ts +839 -0
  28. package/dist/core/StoreManager.js +2034 -0
  29. package/dist/core/SyncConnection.d.ts +105 -0
  30. package/dist/core/SyncConnection.js +348 -0
  31. package/dist/core/Transaction.d.ts +114 -0
  32. package/dist/core/Transaction.js +147 -0
  33. package/dist/core/TransactionQueue.d.ts +110 -0
  34. package/dist/core/TransactionQueue.js +601 -0
  35. package/dist/core/decorators.d.ts +66 -0
  36. package/dist/core/decorators.js +278 -0
  37. package/dist/core/hash.d.ts +6 -0
  38. package/dist/core/hash.js +12 -0
  39. package/dist/core/index.d.ts +16 -0
  40. package/dist/core/index.js +18 -0
  41. package/dist/core/internal.d.ts +27 -0
  42. package/dist/core/internal.js +25 -0
  43. package/dist/core/observability.d.ts +21 -0
  44. package/dist/core/observability.js +66 -0
  45. package/dist/core/refAccessors.d.ts +43 -0
  46. package/dist/core/refAccessors.js +80 -0
  47. package/dist/core/serializers.d.ts +2 -0
  48. package/dist/core/serializers.js +2 -0
  49. package/dist/core/types.d.ts +320 -0
  50. package/dist/core/types.js +84 -0
  51. package/dist/react/index.d.ts +82 -0
  52. package/dist/react/index.js +373 -0
  53. package/dist/schema/builders.d.ts +29 -0
  54. package/dist/schema/builders.js +81 -0
  55. package/dist/schema/compile.d.ts +28 -0
  56. package/dist/schema/compile.js +334 -0
  57. package/dist/schema/createStore.d.ts +235 -0
  58. package/dist/schema/createStore.js +264 -0
  59. package/dist/schema/extend.d.ts +46 -0
  60. package/dist/schema/extend.js +6 -0
  61. package/dist/schema/index.d.ts +13 -0
  62. package/dist/schema/index.js +8 -0
  63. package/dist/schema/infer.d.ts +102 -0
  64. package/dist/schema/infer.js +1 -0
  65. package/dist/schema/types.d.ts +76 -0
  66. package/dist/schema/types.js +1 -0
  67. package/dist/schema/zod.d.ts +90 -0
  68. package/dist/schema/zod.js +101 -0
  69. package/package.json +99 -0
@@ -0,0 +1,264 @@
1
+ import { action, computed } from "mobx";
2
+ import { ModelRegistry } from "../core/ModelRegistry";
3
+ import { prop } from "../core/ObjectPool";
4
+ import { installActionMethod, installComputedAccessor, } from "../core/refAccessors";
5
+ import { compileSchema } from "./compile";
6
+ /**
7
+ * Project a `SchemaDef` over a live `StoreManager`. The runtime values are
8
+ * `BaseModel` instances that structurally satisfy the inferred record type;
9
+ * the proxy-based public surface described in the RFC lands later.
10
+ */
11
+ export function createStore(opts) {
12
+ const compiled = compileSchema(opts.schema);
13
+ const sm = opts.storeManager;
14
+ const merged = mergeExtensions(opts.extensions);
15
+ for (const [entityKey, registryName] of compiled.nameByKey) {
16
+ const defs = merged.get(entityKey);
17
+ if (defs == null) {
18
+ continue;
19
+ }
20
+ applyExtension(registryName, defs, sm);
21
+ }
22
+ const store = {
23
+ batch: sm.batch.bind(sm),
24
+ atomic: sm.atomic.bind(sm),
25
+ undo: () => sm.undo(),
26
+ redo: () => sm.redo(),
27
+ get undoDepth() {
28
+ return sm.transactionQueue.undoDepth;
29
+ },
30
+ get redoDepth() {
31
+ return sm.transactionQueue.redoDepth;
32
+ },
33
+ // Dynamic delegate (not `.bind(sm)`) so test-time `vi.spyOn(sm, "runUndoable")`
34
+ // intercepts calls. `bind` would capture the original at construction time.
35
+ runUndoable: ((fn, opts) => sm.runUndoable(fn, opts)),
36
+ };
37
+ for (const [entityKey, registryName] of compiled.nameByKey) {
38
+ store[entityKey] = createEntityNamespace(registryName, sm);
39
+ }
40
+ return store;
41
+ }
42
+ function mergeExtensions(extensions) {
43
+ const out = new Map();
44
+ if (extensions == null) {
45
+ return out;
46
+ }
47
+ for (const ext of extensions) {
48
+ for (const [entityKey, defs] of Object.entries(ext.byEntity)) {
49
+ if (defs == null) {
50
+ continue;
51
+ }
52
+ let bucket = out.get(entityKey);
53
+ if (bucket == null) {
54
+ bucket = { computed: {}, actions: {} };
55
+ out.set(entityKey, bucket);
56
+ }
57
+ Object.assign(bucket.computed, defs.computed);
58
+ Object.assign(bucket.actions, defs.actions);
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function applyExtension(registryName, defs, sm) {
64
+ const meta = ModelRegistry.getModelMeta(registryName);
65
+ if (meta == null) {
66
+ return;
67
+ }
68
+ const prototype = meta.ctor.prototype;
69
+ for (const [name, fn] of Object.entries(defs.computed)) {
70
+ installComputedAccessor(prototype, name, fn);
71
+ meta.computedProps.add(name);
72
+ rebindComputedInstances(sm, registryName, name);
73
+ }
74
+ for (const [name, fn] of Object.entries(defs.actions)) {
75
+ installActionMethod(prototype, name, fn);
76
+ meta.actions.add(name);
77
+ rebindActionInstances(sm, registryName, name);
78
+ }
79
+ }
80
+ function rebindComputedInstances(sm, registryName, name) {
81
+ for (const instance of sm.objectPool.getAll(registryName)) {
82
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), name);
83
+ if (descriptor?.get == null) {
84
+ continue;
85
+ }
86
+ const fn = descriptor.get.bind(instance);
87
+ const memo = computed(fn);
88
+ Object.defineProperty(instance, name, {
89
+ get: () => memo.get(),
90
+ configurable: true,
91
+ });
92
+ }
93
+ }
94
+ function rebindActionInstances(sm, registryName, name) {
95
+ for (const instance of sm.objectPool.getAll(registryName)) {
96
+ const method = Object.getPrototypeOf(instance)[name];
97
+ if (typeof method !== "function") {
98
+ continue;
99
+ }
100
+ Object.defineProperty(instance, name, {
101
+ configurable: true,
102
+ writable: true,
103
+ value: action(method.bind(instance)),
104
+ });
105
+ }
106
+ }
107
+ function createEntityNamespace(registryName, sm) {
108
+ const meta = ModelRegistry.getModelMeta(registryName);
109
+ if (meta == null) {
110
+ throw new Error(`createStore: model "${registryName}" is not in ModelRegistry. ` +
111
+ `Did the schema fail to compile?`);
112
+ }
113
+ const Ctor = meta.ctor;
114
+ const toRecord = (model) => model;
115
+ const recordsFrom = (list) => list.map(toRecord);
116
+ function draft(arg) {
117
+ if (typeof arg === "string") {
118
+ return sm.getOrLoadById(registryName, arg).then((model) => {
119
+ if (model == null) {
120
+ throw noRecordError(registryName, "draft", arg);
121
+ }
122
+ return toRecord(model);
123
+ });
124
+ }
125
+ const instance = new Ctor();
126
+ if (arg != null) {
127
+ instance.hydrate(arg);
128
+ }
129
+ // Make it observable now (still store===null, not pooled) so fields set
130
+ // between `draft(input)` and `save()` route through the property setter,
131
+ // not a shadowing own-property that makeModelObservable would later
132
+ // discard for the hydrated __raw_ value. Trade-off: a non-lazy
133
+ // @Reference / @ReferenceCollection eager-loads at draft() time rather
134
+ // than save() time (same load surface as create, earlier).
135
+ instance.makeModelObservable();
136
+ return toRecord(instance);
137
+ }
138
+ const ns = {
139
+ peek(id) {
140
+ const model = sm.objectPool.getById(registryName, id);
141
+ return model == null ? undefined : toRecord(model);
142
+ },
143
+ has(id) {
144
+ return sm.objectPool.getById(registryName, id) != null;
145
+ },
146
+ peekAll() {
147
+ return recordsFrom(sm.objectPool.getAll(registryName));
148
+ },
149
+ peekByIndex(key, value) {
150
+ return recordsFrom(sm.peekByIndex(registryName, key, value));
151
+ },
152
+ async get(id) {
153
+ const model = await sm.getOrLoadById(registryName, id);
154
+ return model == null ? null : toRecord(model);
155
+ },
156
+ async getByIds(ids) {
157
+ const list = await sm.getOrLoadByIds(registryName, [...ids]);
158
+ return recordsFrom(list);
159
+ },
160
+ async getByIndex(key, value) {
161
+ const list = await sm.getOrLoadCollection(registryName, key, value);
162
+ return recordsFrom(list);
163
+ },
164
+ async getByIndexValues(key, values) {
165
+ // Fan out in parallel; getOrLoadCollection is a no-op for already-covered
166
+ // (key, value) pairs, so re-firing for stale buckets is cheap.
167
+ await Promise.all(values.map((v) => sm.getOrLoadCollection(registryName, key, v)));
168
+ // After every bucket is loaded, walk the pool once per value to
169
+ // preserve input order, deduping records that match multiple values.
170
+ const seen = new Set();
171
+ const out = [];
172
+ for (const v of values) {
173
+ for (const m of sm.peekByIndex(registryName, key, v)) {
174
+ if (!seen.has(m.id)) {
175
+ seen.add(m.id);
176
+ out.push(m);
177
+ }
178
+ }
179
+ }
180
+ return recordsFrom(out);
181
+ },
182
+ async getAll() {
183
+ const list = await sm.getOrLoadAll(registryName);
184
+ return recordsFrom(list);
185
+ },
186
+ create(input) {
187
+ // store===null on a fresh instance, so commitCreate (not assign+save)
188
+ // is the create path; it folds into an open batch/atomic via the queue.
189
+ const instance = new Ctor();
190
+ instance.hydrate(input);
191
+ sm.commitCreate(instance);
192
+ return toRecord(instance);
193
+ },
194
+ patch(id, fields) {
195
+ const model = requireInstance(sm, registryName, id, "patch");
196
+ model.assign(fields);
197
+ model.save();
198
+ return toRecord(model);
199
+ },
200
+ delete(id) {
201
+ const model = requireInstance(sm, registryName, id, "delete");
202
+ sm.deleteModel(model);
203
+ },
204
+ archive(id) {
205
+ const model = requireInstance(sm, registryName, id, "archive");
206
+ sm.archiveModel(model);
207
+ },
208
+ draft,
209
+ seed(records) {
210
+ const seeded = sm.seed(registryName, records);
211
+ return seeded.map(toRecord);
212
+ },
213
+ async refresh(ids) {
214
+ const list = await sm.refreshModels(registryName, [...ids]);
215
+ return recordsFrom(list);
216
+ },
217
+ async refreshAll() {
218
+ await sm.refreshAllOfModel(registryName);
219
+ },
220
+ async refreshByIndex(key, value) {
221
+ // Delegates to StoreManager.refreshCollection which diffs previous vs
222
+ // fresh ids — server-removed records leave the pool, surviving
223
+ // instances are updated in place so held references stay valid.
224
+ const list = await sm.refreshCollection(registryName, key, value);
225
+ return recordsFrom(list);
226
+ },
227
+ watchAll(cb) {
228
+ return sm.objectPool.subscribe(registryName, cb);
229
+ },
230
+ watchByIndex(key, value, cb) {
231
+ return sm.objectPool.subscribe(registryName, (m) => prop(m, key) === value, cb);
232
+ },
233
+ };
234
+ // Stash the registry name on the namespace so the typed React hooks can
235
+ // recover it without forcing every callsite to repeat the string. Hidden
236
+ // from enumeration so it doesn't leak into JSON serialization or hover
237
+ // tooltips on the public surface.
238
+ Object.defineProperty(ns, REGISTRY_NAME, {
239
+ value: registryName,
240
+ enumerable: false,
241
+ writable: false,
242
+ configurable: false,
243
+ });
244
+ return ns;
245
+ }
246
+ /** @internal Symbol carrying the ModelRegistry name on each namespace. */
247
+ export const REGISTRY_NAME = Symbol("syncEngine/registryName");
248
+ /** @internal Read the registry name a namespace was built for. */
249
+ export function entityNamespaceRegistryName(ns) {
250
+ return ns[REGISTRY_NAME];
251
+ }
252
+ /** Shared "namespace write referenced a missing record" error. `scope`
253
+ * qualifies where we looked — pool-only (`requireInstance`) vs. fully
254
+ * resolved (`draft(id)` via getOrLoadById). */
255
+ function noRecordError(registryName, action, id, scope = "") {
256
+ return new Error(`createStore.${registryName}.${action}: no record with id "${id}"${scope}.`);
257
+ }
258
+ function requireInstance(sm, registryName, id, action) {
259
+ const model = sm.objectPool.getById(registryName, id);
260
+ if (model == null) {
261
+ throw noRecordError(registryName, action, id, " in the pool");
262
+ }
263
+ return model;
264
+ }
@@ -0,0 +1,46 @@
1
+ import type { EntityKey, InferEntity } from "./infer";
2
+ import type { SchemaDef } from "./types";
3
+ export type ComputedFn<S extends SchemaDef, K extends EntityKey<S>> = (record: InferEntity<S, K>) => unknown;
4
+ export type ActionFn<S extends SchemaDef, K extends EntityKey<S>> = (record: InferEntity<S, K>, ...args: never[]) => unknown;
5
+ export interface ExtensionDef<S extends SchemaDef, K extends EntityKey<S>> {
6
+ computed?: Record<string, ComputedFn<S, K>>;
7
+ actions?: Record<string, ActionFn<S, K>>;
8
+ }
9
+ export type ExtensionMap<S extends SchemaDef> = {
10
+ [K in EntityKey<S>]?: ExtensionDef<S, K>;
11
+ };
12
+ /**
13
+ * Carries the schema generic and the *literal* `byEntity` shape so that
14
+ * `MergedExtensionMembers` can read each `computed[name]` / `actions[name]`
15
+ * function literal at the type level — that's how computed return types and
16
+ * action signatures end up on the records returned by `store.<entity>`.
17
+ */
18
+ export interface ExtensionDescriptor<S extends SchemaDef, BE extends Record<string, unknown> = ExtensionMap<S>> {
19
+ /** @internal */
20
+ readonly _schema?: S;
21
+ readonly byEntity: BE;
22
+ }
23
+ export declare function extend<S extends SchemaDef, K extends EntityKey<S>, const Defs extends ExtensionDef<S, K>>(schema: S, entityKey: K, defs: Defs): ExtensionDescriptor<S, {
24
+ [P in K]: Defs;
25
+ }>;
26
+ export declare function extend<S extends SchemaDef, const BE extends ExtensionMap<S>>(schema: S, defs: BE): ExtensionDescriptor<S, BE>;
27
+ type ComputedReturnsOf<Defs> = Defs extends {
28
+ computed?: infer C;
29
+ } ? C extends Record<string, (...args: never[]) => unknown> ? {
30
+ readonly [P in keyof C]: ReturnType<C[P]>;
31
+ } : Record<never, never> : Record<never, never>;
32
+ type ActionMethodsOf<Defs> = Defs extends {
33
+ actions?: infer A;
34
+ } ? A extends Record<string, (...args: never[]) => unknown> ? {
35
+ [P in keyof A]: A[P] extends (record: never, ...args: infer Args) => infer R ? (...args: Args) => R : never;
36
+ } : Record<never, never> : Record<never, never>;
37
+ type MembersFromOne<S extends SchemaDef, K extends EntityKey<S>, Ext> = Ext extends ExtensionDescriptor<S, infer BE> ? K extends keyof BE ? ComputedReturnsOf<BE[K]> & ActionMethodsOf<BE[K]> : Record<never, never> : Record<never, never>;
38
+ /**
39
+ * Distribute `MembersFromOne` over each element of the extensions tuple
40
+ * (via `Exts[number]`) and intersect the resulting union — same shape as a
41
+ * tuple-rest recursive walk but without the recursion depth, so schemas with
42
+ * many extension descriptors stay well below TS's instantiation limits.
43
+ */
44
+ type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
45
+ export type MergedExtensionMembers<S extends SchemaDef, K extends EntityKey<S>, Exts extends readonly ExtensionDescriptor<S>[]> = Exts extends readonly [] ? Record<never, never> : UnionToIntersection<MembersFromOne<S, K, Exts[number]>>;
46
+ export {};
@@ -0,0 +1,6 @@
1
+ export function extend(_schema, arg2, arg3) {
2
+ if (typeof arg2 === "string") {
3
+ return { byEntity: { [arg2]: arg3 } };
4
+ }
5
+ return { byEntity: arg2 };
6
+ }
@@ -0,0 +1,13 @@
1
+ export { LoadStrategy } from "../core/types";
2
+ export { defineSchema, entity, link, fields } from "./builders";
3
+ export { fields as s } from "./builders";
4
+ export { compileSchema } from "./compile";
5
+ export type { CompiledSchema } from "./compile";
6
+ export { createStore } from "./createStore";
7
+ export type { EntityStore, EntityNamespace, RecordWithExtensions, StoreApi, } from "./createStore";
8
+ export { extend } from "./extend";
9
+ export type { ActionFn, ComputedFn, ExtensionDef, ExtensionDescriptor, ExtensionMap, MergedExtensionMembers, } from "./extend";
10
+ export { fromZod, entityFromZod } from "./zod";
11
+ export type { EntityFromZodFieldOverride, EntityFromZodOpts } from "./zod";
12
+ export type { EntityDef, FieldBuilder, FieldKind, FieldMeta, LinkDef, LinkFromSpec, LinkToSpec, OnDelete, SchemaDef, } from "./types";
13
+ export type { EntityKey, IndexedFieldKeys, InferCreateInput, InferEntity, InferUpdateInput, RelationCollection, } from "./infer";
@@ -0,0 +1,8 @@
1
+ // Public schema-authoring surface. See agent-docs/RFC-schema-first-authoring.md.
2
+ export { LoadStrategy } from "../core/types";
3
+ export { defineSchema, entity, link, fields } from "./builders";
4
+ export { fields as s } from "./builders";
5
+ export { compileSchema } from "./compile";
6
+ export { createStore } from "./createStore";
7
+ export { extend } from "./extend";
8
+ export { fromZod, entityFromZod } from "./zod";
@@ -0,0 +1,102 @@
1
+ import type { EntityDef, FieldBuilder, FieldMeta, SchemaDef } from "./types";
2
+ type FieldType<F> = F extends FieldBuilder<infer T, FieldMeta> ? T : never;
3
+ type FieldIsNullable<F> = null extends FieldType<F> ? true : false;
4
+ type FieldIsIndexed<F> = F extends FieldBuilder<unknown, infer M> ? M extends {
5
+ indexed: true;
6
+ } ? true : false : false;
7
+ export type EntityKey<S extends SchemaDef> = keyof S["entities"] & string;
8
+ /**
9
+ * Field keys on an entity that were declared with `.indexed()`. Used to
10
+ * constrain `store.<entity>.loadByIndex(key, value)` so callers can only pass
11
+ * indexes that actually exist on disk.
12
+ */
13
+ export type IndexedFieldKeys<S extends SchemaDef, K extends EntityKey<S>> = {
14
+ [P in keyof EntityFieldsRecord<S, K>]: FieldIsIndexed<EntityFieldsRecord<S, K>[P]> extends true ? P : never;
15
+ }[keyof EntityFieldsRecord<S, K>] & string;
16
+ type EntityFieldsRecord<S extends SchemaDef, K extends EntityKey<S>> = S["entities"][K] extends EntityDef<infer F> ? F : never;
17
+ /**
18
+ * Stub shape for the reactive lazy-collection wrapper that today's
19
+ * `RefCollection` / `BackRef` runtime classes return. The schema-typed
20
+ * record can't extend `BaseModel` without coupling the type wall the
21
+ * proxy is meant to abstract, so we expose a narrow interface here and
22
+ * project `RefCollection` onto it when the typed client lands (Phase 3).
23
+ */
24
+ export interface RelationCollection<T> {
25
+ load(): Promise<readonly T[]>;
26
+ readonly items: readonly T[];
27
+ /**
28
+ * Fires whenever the collection's items list changes (records added,
29
+ * removed, or replaced). Payload-less — re-read `items` inside `cb`.
30
+ * Returns an unsubscribe function. Same verb as `record.watch` /
31
+ * `store.<entity>.watchAll`.
32
+ */
33
+ watch(cb: () => void): () => void;
34
+ }
35
+ type EntityFieldTypes<S extends SchemaDef, K extends EntityKey<S>> = {
36
+ -readonly [P in keyof EntityFieldsRecord<S, K>]: FieldType<EntityFieldsRecord<S, K>[P]>;
37
+ };
38
+ type SingularRelationKey<S extends SchemaDef, K extends EntityKey<S>, LK extends keyof S["links"]> = S["links"][LK] extends {
39
+ from: {
40
+ entity: K;
41
+ as: infer A extends string;
42
+ };
43
+ } ? A : never;
44
+ type SingularRelationValue<S extends SchemaDef, K extends EntityKey<S>, LK extends keyof S["links"]> = S["links"][LK] extends {
45
+ from: {
46
+ entity: K;
47
+ field: infer FFK;
48
+ };
49
+ to: {
50
+ entity: infer TE extends string;
51
+ };
52
+ } ? TE extends EntityKey<S> ? FFK extends keyof EntityFieldsRecord<S, K> ? FieldIsNullable<EntityFieldsRecord<S, K>[FFK]> extends true ? InferEntity<S, TE> | null : InferEntity<S, TE> : never : never : never;
53
+ type SingularRelations<S extends SchemaDef, K extends EntityKey<S>> = {
54
+ [LK in keyof S["links"] as SingularRelationKey<S, K, LK>]: SingularRelationValue<S, K, LK>;
55
+ };
56
+ type ReverseCollectionKey<S extends SchemaDef, K extends EntityKey<S>, LK extends keyof S["links"]> = S["links"][LK] extends {
57
+ to: {
58
+ entity: K;
59
+ many: infer M extends string;
60
+ };
61
+ } ? M : never;
62
+ type ReverseCollectionValue<S extends SchemaDef, LK extends keyof S["links"]> = S["links"][LK] extends {
63
+ from: {
64
+ entity: infer FE extends string;
65
+ };
66
+ } ? FE extends EntityKey<S> ? RelationCollection<InferEntity<S, FE>> : never : never;
67
+ type ReverseCollections<S extends SchemaDef, K extends EntityKey<S>> = {
68
+ [LK in keyof S["links"] as ReverseCollectionKey<S, K, LK>]: ReverseCollectionValue<S, LK>;
69
+ };
70
+ /**
71
+ * The record shape for a schema entity: declared fields, plus singular
72
+ * relation properties for every link that originates on this entity, plus
73
+ * reverse-collection properties for every link that targets it.
74
+ *
75
+ * Does not include extension members (computed / actions) — those are layered
76
+ * in by `InferRecord` once `extend(...)` lands.
77
+ *
78
+ * Returned as a plain intersection — wrapping in a `Prettify` mapped type
79
+ * would force TS to materialize a fresh object per relation step every time
80
+ * `InferEntity` recurses through a link. Users can pretty their own aliases
81
+ * with a one-line helper at the call site if they want.
82
+ */
83
+ export type InferEntity<S extends SchemaDef, K extends EntityKey<S>> = EntityFieldTypes<S, K> & SingularRelations<S, K> & ReverseCollections<S, K>;
84
+ /**
85
+ * A create-input field is optional when the runtime can fill it without
86
+ * the caller: id-kind (BaseModel auto-assigns a UUID), defaulted fields,
87
+ * and schema fields explicitly marked optional (e.g. via Zod's `.optional()`).
88
+ */
89
+ type IsOptionalCreateField<F> = F extends FieldBuilder<unknown, infer M> ? M extends {
90
+ kind: "id";
91
+ } | {
92
+ default: unknown;
93
+ } ? true : M extends {
94
+ optional: true;
95
+ } ? true : false : false;
96
+ export type InferCreateInput<S extends SchemaDef, K extends EntityKey<S>> = {
97
+ [P in keyof EntityFieldsRecord<S, K> as IsOptionalCreateField<EntityFieldsRecord<S, K>[P]> extends true ? never : P]: FieldType<EntityFieldsRecord<S, K>[P]>;
98
+ } & {
99
+ [P in keyof EntityFieldsRecord<S, K> as IsOptionalCreateField<EntityFieldsRecord<S, K>[P]> extends true ? P : never]?: FieldType<EntityFieldsRecord<S, K>[P]>;
100
+ };
101
+ export type InferUpdateInput<S extends SchemaDef, K extends EntityKey<S>> = Partial<EntityFieldTypes<S, K>>;
102
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import type { LoadStrategy, OnDelete } from "../core/types";
2
+ export type FieldKind = "id" | "string" | "number" | "boolean" | "date" | "json" | "refId";
3
+ export interface FieldMeta {
4
+ kind: FieldKind;
5
+ nullable: boolean;
6
+ optional: boolean;
7
+ indexed: boolean;
8
+ ephemeral: boolean;
9
+ default?: unknown;
10
+ refTarget?: string;
11
+ serializer?: (value: unknown) => unknown;
12
+ deserializer?: (raw: unknown) => unknown;
13
+ }
14
+ /**
15
+ * Carries two pieces of information through the type system:
16
+ * T — the TS type the field exposes (`string`, `number | null`, `Date`, …).
17
+ * `InferEntity` reads this to build the record shape.
18
+ * M — the runtime metadata, with literal-preserved fields like
19
+ * `kind: "refId"` and `refTarget: "team"` so `link(...)` / inference
20
+ * can pull relation targets back out of the schema at the type level.
21
+ */
22
+ export interface FieldBuilder<T, M extends FieldMeta = FieldMeta> {
23
+ /** @internal — phantom slot used only for type inference. */
24
+ readonly _t?: T;
25
+ readonly meta: M;
26
+ nullable(): FieldBuilder<T | null, M>;
27
+ indexed(): FieldBuilder<T, Omit<M, "indexed"> & {
28
+ indexed: true;
29
+ }>;
30
+ default(value: T): FieldBuilder<T, Omit<M, "default"> & {
31
+ default: T;
32
+ }>;
33
+ ephemeral(): FieldBuilder<T, M>;
34
+ serialize(fn: (value: T) => unknown): FieldBuilder<T, M>;
35
+ deserialize(fn: (raw: unknown) => T): FieldBuilder<T, M>;
36
+ }
37
+ export type AnyFieldBuilder = FieldBuilder<unknown, FieldMeta>;
38
+ export interface EntityDef<F extends Record<string, AnyFieldBuilder> = Record<string, AnyFieldBuilder>> {
39
+ loadStrategy: LoadStrategy;
40
+ usedForPartialIndexes?: boolean;
41
+ /** Override the registry name. Defaults to PascalCase of the entity key at compile time. */
42
+ name?: string;
43
+ /** Forces a schemaVersion override. Otherwise computed by the compiler. */
44
+ version?: number;
45
+ /**
46
+ * Marks this entity as registered elsewhere (typically by `@ClientModel`).
47
+ * The compiler skips class generation and property/link registration for
48
+ * external entities, but still allows other schema entities to reference
49
+ * them via `s.refId(...)` and `link({ to: { entity: ... } })`. `name` must
50
+ * be set explicitly so the schema can resolve cross-references against the
51
+ * existing registry entry.
52
+ */
53
+ external?: boolean;
54
+ fields: F;
55
+ }
56
+ export interface LinkFromSpec<Entity extends string = string, Field extends string = string, As extends string = string> {
57
+ entity: Entity;
58
+ field: Field;
59
+ as: As;
60
+ }
61
+ export interface LinkToSpec<Entity extends string = string, Many extends string = string> {
62
+ entity: Entity;
63
+ many: Many;
64
+ lazy?: boolean;
65
+ }
66
+ export type { OnDelete };
67
+ export interface LinkDef<FromEntity extends string = string, FromField extends string = string, As extends string = string, ToEntity extends string = string, Many extends string = string> {
68
+ from: LinkFromSpec<FromEntity, FromField, As>;
69
+ to: LinkToSpec<ToEntity, Many>;
70
+ onDelete?: OnDelete;
71
+ }
72
+ export type AnyLinkDef = LinkDef;
73
+ export interface SchemaDef<E extends Record<string, EntityDef> = Record<string, EntityDef>, L extends Record<string, AnyLinkDef> = Record<string, AnyLinkDef>> {
74
+ entities: E;
75
+ links: L;
76
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import type { z } from "zod";
2
+ import type { AnyFieldBuilder, EntityDef, FieldBuilder } from "./types";
3
+ /**
4
+ * Convert any Zod schema into the equivalent schema-first `FieldBuilder`.
5
+ * Handles the common nullable / optional / default modifiers; anything more
6
+ * structured (objects, arrays, unions, enums) collapses to `s.json<T>()` so
7
+ * the runtime stores the raw value and the TS type still flows from Zod.
8
+ *
9
+ * Zod is an optional peer dependency — calling this function requires the
10
+ * caller to have installed `zod`.
11
+ */
12
+ export declare function fromZod<Z extends z.ZodType>(zSchema: Z): FieldBuilder<z.infer<Z>>;
13
+ /**
14
+ * Per-field override for `entityFromZod`. Either a chaining function
15
+ * (modifies the auto-derived `FieldBuilder`) or a full `FieldBuilder`
16
+ * (replaces it — useful for FKs and other shapes Zod can't model).
17
+ */
18
+ export type EntityFromZodFieldOverride<AutoT = unknown> = AnyFieldBuilder | ((auto: FieldBuilder<AutoT>) => AnyFieldBuilder);
19
+ type EntityFromZodFieldOverrides<Z extends z.ZodObject> = {
20
+ [K in keyof z.infer<Z> & string]?: AnyFieldBuilder | ((auto: FieldBuilder<z.infer<Z>[K]>) => unknown);
21
+ };
22
+ type NoExtraZodFieldKeys<Z extends z.ZodObject, F> = Record<Exclude<keyof F, keyof z.infer<Z> & string>, never>;
23
+ /**
24
+ * Resolve the field type contributed by an override entry. Functions are
25
+ * unwrapped via their inferred return type so chained modifiers like
26
+ * `.indexed()` carry their narrowed `M` into the entity's inferred fields;
27
+ * direct `FieldBuilder` overrides are used as-is. When no override is
28
+ * provided the auto-derived `FieldBuilder<AutoT>` from Zod stands.
29
+ */
30
+ type FieldFromOverride<O, AutoT> = O extends (...args: never[]) => infer R ? R extends FieldBuilder<infer RT, infer RM> ? [unknown] extends [RT] ? FieldBuilder<AutoT, RM> : R : FieldBuilder<AutoT> : O extends AnyFieldBuilder ? O : FieldBuilder<AutoT>;
31
+ /**
32
+ * Per-key merge of the Zod-inferred fields with `opts.fields` overrides.
33
+ * Override metadata (`.indexed()`, refId target, …) propagates into the
34
+ * entity's TS type so downstream helpers like `IndexedFieldKeys` see them.
35
+ */
36
+ type MergedFieldsFromZodObject<Z extends z.ZodObject, F> = {
37
+ [K in keyof z.infer<Z>]: K extends keyof F ? FieldFromOverride<F[K], z.infer<Z>[K]> : FieldBuilder<z.infer<Z>[K]>;
38
+ };
39
+ /** Non-`fields` portion of the opts — shared across the public type and
40
+ * the function's inferred-`F` signature. Tracks `EntityDef` so any new
41
+ * top-level knob (like `external`) has to be added here intentionally. */
42
+ type EntityFromZodOptsBase = Pick<EntityDef, "loadStrategy" | "usedForPartialIndexes" | "name" | "version">;
43
+ export interface EntityFromZodOpts<Z extends z.ZodObject = z.ZodObject> extends EntityFromZodOptsBase {
44
+ /**
45
+ * Per-field overrides applied after the Zod-derived `FieldBuilder`.
46
+ * Keys are constrained to fields actually declared on the Zod object,
47
+ * so typos surface at compile time.
48
+ *
49
+ * entityFromZod(ZodIssue, {
50
+ * loadStrategy: LoadStrategy.Eager,
51
+ * fields: {
52
+ * teamId: s.refId("team").nullable().indexed(), // replace
53
+ * email: (b) => b.indexed(), // chain
54
+ * draftNote: (b) => b.ephemeral(),
55
+ * },
56
+ * });
57
+ */
58
+ fields?: {
59
+ [K in keyof z.infer<Z> & string]?: EntityFromZodFieldOverride<z.infer<Z>[K]>;
60
+ };
61
+ }
62
+ /**
63
+ * Convert a `z.object({ ... })` into an `EntityDef`. Each key on the Zod
64
+ * object becomes a field via `fromZod`, then `opts.fields[key]` (if
65
+ * present) is applied — either chained onto the auto-derived builder, or
66
+ * used as a full replacement.
67
+ *
68
+ * The override map is captured via a `const`-modified generic so per-field
69
+ * metadata (`.indexed()`, refId target, …) flows into the returned
70
+ * `EntityDef` — `IndexedFieldKeys`, `getByIndex`, `peekByIndex`, and the
71
+ * typed React hooks all see Zod-built entities the same way they see
72
+ * hand-written ones.
73
+ *
74
+ * `F` is inferred from the provided `fields` map while an intersected
75
+ * override-map type supplies allowed keys and key-specific contextual typing
76
+ * for the `(auto) => ...` parameter. When no `fields` map is provided, `F`
77
+ * defaults to an empty record so untouched Zod fields stay on the auto-derived
78
+ * `FieldBuilder<z.infer<...>>` path.
79
+ * `EntityFromZodOpts<Z>` keeps the strict union for users who pre-type
80
+ * their opts variables.
81
+ *
82
+ * Only single-record entities are produced; relations still belong in the
83
+ * schema's `links` block. Treat the Zod object as the source of field
84
+ * shape and validation; `link(...)` remains the source of truth for the
85
+ * graph.
86
+ */
87
+ export declare function entityFromZod<Z extends z.ZodObject, const F = Record<never, never>>(zSchema: Z, opts: EntityFromZodOptsBase & {
88
+ fields?: F & EntityFromZodFieldOverrides<Z> & NoExtraZodFieldKeys<Z, F>;
89
+ }): EntityDef<MergedFieldsFromZodObject<Z, F>>;
90
+ export {};