@confect/core 9.2.2 → 10.0.0-next.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 (42) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/FunctionProvenance.d.ts +30 -30
  3. package/dist/FunctionProvenance.d.ts.map +1 -1
  4. package/dist/FunctionProvenance.js.map +1 -1
  5. package/dist/FunctionSpec.d.ts +14 -14
  6. package/dist/FunctionSpec.d.ts.map +1 -1
  7. package/dist/FunctionSpec.js.map +1 -1
  8. package/dist/GenericId.d.ts +2 -2
  9. package/dist/GenericId.d.ts.map +1 -1
  10. package/dist/GenericId.js +4 -3
  11. package/dist/GenericId.js.map +1 -1
  12. package/dist/PaginationResult.d.ts +6 -10
  13. package/dist/PaginationResult.d.ts.map +1 -1
  14. package/dist/PaginationResult.js +6 -2
  15. package/dist/PaginationResult.js.map +1 -1
  16. package/dist/Ref.d.ts +5 -5
  17. package/dist/Ref.d.ts.map +1 -1
  18. package/dist/Ref.js +10 -10
  19. package/dist/Ref.js.map +1 -1
  20. package/dist/Refs.js +1 -1
  21. package/dist/Registry.d.ts +1 -4
  22. package/dist/Registry.d.ts.map +1 -1
  23. package/dist/Registry.js +1 -1
  24. package/dist/Registry.js.map +1 -1
  25. package/dist/SystemFields.d.ts +51 -10
  26. package/dist/SystemFields.d.ts.map +1 -1
  27. package/dist/SystemFields.js +79 -2
  28. package/dist/SystemFields.js.map +1 -1
  29. package/dist/UserIdentity.d.ts +20 -54
  30. package/dist/UserIdentity.d.ts.map +1 -1
  31. package/dist/UserIdentity.js +17 -17
  32. package/dist/UserIdentity.js.map +1 -1
  33. package/dist/tsconfig.src.tsbuildinfo +1 -1
  34. package/package.json +4 -4
  35. package/src/FunctionProvenance.ts +12 -12
  36. package/src/FunctionSpec.ts +9 -9
  37. package/src/GenericId.ts +7 -7
  38. package/src/PaginationResult.ts +5 -8
  39. package/src/Ref.ts +21 -16
  40. package/src/Registry.ts +3 -3
  41. package/src/SystemFields.ts +255 -26
  42. package/src/UserIdentity.ts +17 -17
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@confect/core",
3
3
  "description": "Shared specs and schemas used by all Confect packages",
4
- "version": "9.2.2",
4
+ "version": "10.0.0-next.0",
5
5
  "author": "RJ Dellecese",
6
6
  "bugs": {
7
7
  "url": "https://github.com/rjdellecese/confect/issues"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@ark/attest": "0.56.0",
11
- "@effect/vitest": "0.29.0",
11
+ "@effect/vitest": "4.0.0-beta.97",
12
12
  "@types/node": "25.3.3",
13
- "effect": "3.21.2",
13
+ "effect": "4.0.0-beta.97",
14
14
  "tsdown": "0.20.3",
15
15
  "tsx": "4.21.0",
16
16
  "typescript": "5.9.3",
@@ -49,7 +49,7 @@
49
49
  "module": "./dist/index.js",
50
50
  "peerDependencies": {
51
51
  "convex": "^1.32.0",
52
- "effect": "^3.21.2"
52
+ "effect": "^4.0.0-beta.97"
53
53
  },
54
54
  "repository": {
55
55
  "type": "git",
@@ -5,9 +5,9 @@ import * as Lazy from "./Lazy";
5
5
 
6
6
  export type FunctionProvenance = Data.TaggedEnum<{
7
7
  Confect: {
8
- args: Schema.Schema.AnyNoContext;
9
- returns: Schema.Schema.AnyNoContext;
10
- error?: Schema.Schema.AnyNoContext;
8
+ args: Schema.Codec<any, any>;
9
+ returns: Schema.Codec<any, any>;
10
+ error?: Schema.Codec<any, any>;
11
11
  };
12
12
  Convex: {
13
13
  /** @internal */
@@ -18,9 +18,9 @@ export type FunctionProvenance = Data.TaggedEnum<{
18
18
  }>;
19
19
 
20
20
  export interface Confect<
21
- Args extends Schema.Schema.AnyNoContext,
22
- Returns extends Schema.Schema.AnyNoContext,
23
- Error extends Schema.Schema.AnyNoContext = never,
21
+ Args extends Schema.Codec<any, any>,
22
+ Returns extends Schema.Codec<any, any>,
23
+ Error extends Schema.Codec<any, any> = never,
24
24
  > {
25
25
  readonly _tag: "Confect";
26
26
  readonly args: Args;
@@ -29,9 +29,9 @@ export interface Confect<
29
29
  }
30
30
 
31
31
  export interface AnyConfect extends Confect<
32
- Schema.Schema.AnyNoContext,
33
- Schema.Schema.AnyNoContext,
34
- Schema.Schema.AnyNoContext
32
+ Schema.Codec<any, any>,
33
+ Schema.Codec<any, any>,
34
+ Schema.Codec<any, any>
35
35
  > {}
36
36
 
37
37
  export interface Convex<Args extends DefaultFunctionArgs, Returns> {
@@ -61,9 +61,9 @@ export const FunctionProvenance = Data.taggedEnum<FunctionProvenance>();
61
61
  * relies on `Data`'s structural `Equal`/`Hash` for provenance values.
62
62
  */
63
63
  export const Confect = <
64
- Args extends Schema.Schema.AnyNoContext,
65
- Returns extends Schema.Schema.AnyNoContext,
66
- Error extends Schema.Schema.AnyNoContext = never,
64
+ Args extends Schema.Codec<any, any>,
65
+ Returns extends Schema.Codec<any, any>,
66
+ Error extends Schema.Codec<any, any> = never,
67
67
  >(
68
68
  args: () => Args,
69
69
  returns: () => Returns,
@@ -93,7 +93,7 @@ export type Name<FunctionSpec_ extends AnyWithProps> = FunctionSpec_["name"];
93
93
  export type Args<FunctionSpec_ extends AnyWithProps> = FunctionSpec_ extends {
94
94
  functionProvenance: {
95
95
  _tag: "Confect";
96
- args: infer ArgsSchema_ extends Schema.Schema.AnyNoContext;
96
+ args: infer ArgsSchema_ extends Schema.Codec<any, any>;
97
97
  };
98
98
  }
99
99
  ? ArgsSchema_["Type"]
@@ -107,7 +107,7 @@ export type Returns<FunctionSpec_ extends AnyWithProps> =
107
107
  FunctionSpec_ extends {
108
108
  functionProvenance: {
109
109
  _tag: "Confect";
110
- returns: infer ReturnsSchema_ extends Schema.Schema.AnyNoContext;
110
+ returns: infer ReturnsSchema_ extends Schema.Codec<any, any>;
111
111
  };
112
112
  }
113
113
  ? ReturnsSchema_["Type"]
@@ -121,7 +121,7 @@ export type EncodedArgs<FunctionSpec_ extends AnyWithProps> =
121
121
  FunctionSpec_ extends {
122
122
  functionProvenance: {
123
123
  _tag: "Confect";
124
- args: infer ArgsSchema_ extends Schema.Schema.AnyNoContext;
124
+ args: infer ArgsSchema_ extends Schema.Codec<any, any>;
125
125
  };
126
126
  }
127
127
  ? ArgsSchema_["Encoded"]
@@ -135,7 +135,7 @@ export type EncodedReturns<FunctionSpec_ extends AnyWithProps> =
135
135
  FunctionSpec_ extends {
136
136
  functionProvenance: {
137
137
  _tag: "Confect";
138
- returns: infer ReturnsSchema_ extends Schema.Schema.AnyNoContext;
138
+ returns: infer ReturnsSchema_ extends Schema.Codec<any, any>;
139
139
  };
140
140
  }
141
141
  ? ReturnsSchema_["Encoded"]
@@ -149,7 +149,7 @@ export type Error<FunctionSpec_ extends AnyWithProps> = FunctionSpec_ extends {
149
149
  functionProvenance: FunctionProvenance.Confect<
150
150
  any,
151
151
  any,
152
- infer ErrorSchema_ extends Schema.Schema.AnyNoContext
152
+ infer ErrorSchema_ extends Schema.Codec<any, any>
153
153
  >;
154
154
  }
155
155
  ? ErrorSchema_["Type"]
@@ -160,7 +160,7 @@ export type EncodedError<FunctionSpec_ extends AnyWithProps> =
160
160
  functionProvenance: FunctionProvenance.Confect<
161
161
  any,
162
162
  any,
163
- infer ErrorSchema_ extends Schema.Schema.AnyNoContext
163
+ infer ErrorSchema_ extends Schema.Codec<any, any>
164
164
  >;
165
165
  }
166
166
  ? ErrorSchema_["Encoded"]
@@ -215,9 +215,9 @@ const make =
215
215
  ) =>
216
216
  <
217
217
  const Name_ extends string,
218
- Args_ extends Schema.Schema.AnyNoContext,
219
- Returns_ extends Schema.Schema.AnyNoContext,
220
- Error_ extends Schema.Schema.AnyNoContext = never,
218
+ Args_ extends Schema.Codec<any, any>,
219
+ Returns_ extends Schema.Codec<any, any>,
220
+ Error_ extends Schema.Codec<any, any> = never,
221
221
  >({
222
222
  name,
223
223
  args,
package/src/GenericId.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  import type { GenericId as ConvexGenericId } from "convex/values";
2
- import type { Option } from "effect";
2
+ import * as Option from "effect/Option";
3
3
  import * as Schema from "effect/Schema";
4
4
  import * as SchemaAST from "effect/SchemaAST";
5
5
 
6
- const ConvexId = Symbol.for("ConvexId");
6
+ const ConvexId = "~@confect/core/ConvexId";
7
7
 
8
8
  export const GenericId = <TableName extends string>(
9
9
  tableName: TableName,
10
- ): Schema.Schema<ConvexGenericId<TableName>> =>
11
- Schema.String.pipe(
12
- Schema.annotations({ [ConvexId]: tableName }),
13
- ) as unknown as Schema.Schema<ConvexGenericId<TableName>>;
10
+ ): Schema.Codec<ConvexGenericId<TableName>> =>
11
+ Schema.String.annotate({
12
+ [ConvexId]: tableName,
13
+ }) as unknown as Schema.Codec<ConvexGenericId<TableName>>;
14
14
 
15
15
  export type GenericId<TableName extends string> = ConvexGenericId<TableName>;
16
16
 
17
17
  export const tableName = <TableName extends string>(
18
18
  ast: SchemaAST.AST,
19
19
  ): Option.Option<TableName> =>
20
- SchemaAST.getAnnotation<TableName>(ConvexId)(ast);
20
+ Option.fromNullishOr(SchemaAST.resolveAt<TableName>(ConvexId)(ast));
@@ -1,21 +1,18 @@
1
1
  import * as Schema from "effect/Schema";
2
2
 
3
- export const PaginationResult = <Doc extends Schema.Schema.AnyNoContext>(
3
+ export const PaginationResult = <Doc extends Schema.Codec<any, any>>(
4
4
  Doc: Doc,
5
5
  ) =>
6
6
  Schema.Struct({
7
7
  page: Schema.mutable(Schema.Array(Doc)),
8
8
  isDone: Schema.Boolean,
9
9
  continueCursor: Schema.String,
10
- splitCursor: Schema.optionalWith(Schema.Union(Schema.String, Schema.Null), {
11
- exact: true,
12
- }),
13
- pageStatus: Schema.optionalWith(
14
- Schema.Union(
10
+ splitCursor: Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])),
11
+ pageStatus: Schema.optionalKey(
12
+ Schema.Union([
15
13
  Schema.Literal("SplitRecommended"),
16
14
  Schema.Literal("SplitRequired"),
17
15
  Schema.Null,
18
- ),
19
- { exact: true },
16
+ ]),
20
17
  ),
21
18
  });
package/src/Ref.ts CHANGED
@@ -5,7 +5,6 @@ import type {
5
5
  import { makeFunctionReference } from "convex/server";
6
6
  import type { Value } from "convex/values";
7
7
  import { ConvexError } from "convex/values";
8
- import type { ParseResult } from "effect";
9
8
  import * as Effect from "effect/Effect";
10
9
  import * as Match from "effect/Match";
11
10
  import * as Option from "effect/Option";
@@ -224,10 +223,10 @@ export const hasErrorSchema = (ref: Any): boolean =>
224
223
  export const encodeArgs = <Ref_ extends Any>(
225
224
  ref: Ref_,
226
225
  args: Args<Ref_>,
227
- ): Effect.Effect<unknown, ParseResult.ParseError> =>
226
+ ): Effect.Effect<unknown, Schema.SchemaError> =>
228
227
  Match.value(ref.functionSpec.functionProvenance).pipe(
229
228
  Match.tag("Confect", (confectFunctionProvenance) =>
230
- Schema.encode(confectFunctionProvenance.args)(args),
229
+ Schema.encodeEffect(confectFunctionProvenance.args)(args),
231
230
  ),
232
231
  Match.tag("Convex", () => Effect.succeed(args)),
233
232
  Match.exhaustive,
@@ -236,10 +235,10 @@ export const encodeArgs = <Ref_ extends Any>(
236
235
  export const decodeReturns = <Ref_ extends Any>(
237
236
  ref: Ref_,
238
237
  returns: unknown,
239
- ): Effect.Effect<Returns<Ref_>, ParseResult.ParseError> =>
238
+ ): Effect.Effect<Returns<Ref_>, Schema.SchemaError> =>
240
239
  Match.value(ref.functionSpec.functionProvenance).pipe(
241
240
  Match.tag("Confect", (confectFunctionProvenance) =>
242
- Schema.decode(confectFunctionProvenance.returns)(returns),
241
+ Schema.decodeUnknownEffect(confectFunctionProvenance.returns)(returns),
243
242
  ),
244
243
  Match.tag("Convex", () => Effect.succeed(returns)),
245
244
  Match.exhaustive,
@@ -263,7 +262,7 @@ export const decodeArgsSync = <Ref_ extends Any>(
263
262
  ): Args<Ref_> =>
264
263
  Match.value(ref.functionSpec.functionProvenance).pipe(
265
264
  Match.tag("Confect", (confectFunctionProvenance) =>
266
- Schema.decodeSync(confectFunctionProvenance.args)(encodedArgs),
265
+ Schema.decodeUnknownSync(confectFunctionProvenance.args)(encodedArgs),
267
266
  ),
268
267
  Match.tag("Convex", () => encodedArgs),
269
268
  Match.exhaustive,
@@ -287,7 +286,9 @@ export const decodeReturnsSync = <Ref_ extends Any>(
287
286
  ): Returns<Ref_> =>
288
287
  Match.value(ref.functionSpec.functionProvenance).pipe(
289
288
  Match.tag("Confect", (confectFunctionProvenance) =>
290
- Schema.decodeSync(confectFunctionProvenance.returns)(encodedReturns),
289
+ Schema.decodeUnknownSync(confectFunctionProvenance.returns)(
290
+ encodedReturns,
291
+ ),
291
292
  ),
292
293
  Match.tag("Convex", () => encodedReturns),
293
294
  Match.exhaustive,
@@ -332,12 +333,14 @@ export const decodeErrorOrElse =
332
333
  export const decodeError = <Ref_ extends Any>(
333
334
  ref: Ref_,
334
335
  encodedError: unknown,
335
- ): Effect.Effect<Option.Option<Error<Ref_>>, ParseResult.ParseError> =>
336
+ ): Effect.Effect<Option.Option<Error<Ref_>>, Schema.SchemaError> =>
336
337
  Match.value(ref.functionSpec.functionProvenance).pipe(
337
338
  Match.tag("Confect", (confectFunctionProvenance) =>
338
339
  "error" in confectFunctionProvenance
339
340
  ? Effect.map(
340
- Schema.decode(confectFunctionProvenance.error)(encodedError),
341
+ Schema.decodeUnknownEffect(confectFunctionProvenance.error)(
342
+ encodedError,
343
+ ),
341
344
  Option.some,
342
345
  )
343
346
  : Effect.succeed(Option.none<Error<Ref_>>()),
@@ -358,7 +361,7 @@ export const decodeErrorSync = <Ref_ extends Any>(
358
361
  Match.tag("Confect", (confectFunctionProvenance) =>
359
362
  "error" in confectFunctionProvenance
360
363
  ? Option.some(
361
- Schema.decodeSync(confectFunctionProvenance.error)(
364
+ Schema.decodeUnknownSync(confectFunctionProvenance.error)(
362
365
  encodedError,
363
366
  ) as Error<Ref_>,
364
367
  )
@@ -376,7 +379,9 @@ export const maybeDecodeErrorSync = <Ref_ extends Any>(
376
379
  ? Match.value(ref.functionSpec.functionProvenance).pipe(
377
380
  Match.tag("Confect", (confectFunctionProvenance) =>
378
381
  "error" in confectFunctionProvenance
379
- ? Schema.decodeSync(confectFunctionProvenance.error)(error.data)
382
+ ? Schema.decodeUnknownSync(confectFunctionProvenance.error)(
383
+ error.data,
384
+ )
380
385
  : error,
381
386
  ),
382
387
  Match.tag("Convex", () => error),
@@ -400,7 +405,7 @@ export const runWithCodec = <Ref_ extends Any, E = never>(
400
405
  encodedArgs: unknown,
401
406
  ) => PromiseLike<unknown>,
402
407
  mapUnknownError?: (error: unknown) => E,
403
- ): Effect.Effect<Returns<Ref_>, E | Error<Ref_> | ParseResult.ParseError> =>
408
+ ): Effect.Effect<Returns<Ref_>, E | Error<Ref_> | Schema.SchemaError> =>
404
409
  Effect.gen(function* () {
405
410
  const functionReference = getFunctionReference(ref);
406
411
  const functionProvenance = ref.functionSpec.functionProvenance;
@@ -425,13 +430,13 @@ export const runWithCodec = <Ref_ extends Any, E = never>(
425
430
  return yield* Match.value(functionProvenance).pipe(
426
431
  Match.tag("Confect", (confectFunctionProvenance) =>
427
432
  Effect.gen(function* () {
428
- const encodedArgs = yield* Schema.encode(
433
+ const encodedArgs = yield* Schema.encodeEffect(
429
434
  confectFunctionProvenance.args,
430
435
  )(args);
431
436
  const encodedReturns = yield* invoke(encodedArgs);
432
- return yield* Schema.decode(confectFunctionProvenance.returns)(
433
- encodedReturns,
434
- );
437
+ return yield* Schema.decodeUnknownEffect(
438
+ confectFunctionProvenance.returns,
439
+ )(encodedReturns);
435
440
  }),
436
441
  ),
437
442
  Match.tag("Convex", () => invoke(args)),
package/src/Registry.ts CHANGED
@@ -22,9 +22,9 @@ export interface RegistryItems {
22
22
  * Effect's global default-value cache to align two separately-defined tags
23
23
  * by string key.
24
24
  */
25
- export class Registry extends Context.Reference<Registry>()(
25
+ export const Registry = Context.Reference<Ref.Ref<RegistryItems>>(
26
26
  "@confect/core/Registry",
27
27
  {
28
- defaultValue: () => Ref.unsafeMake<RegistryItems>({}),
28
+ defaultValue: () => Ref.makeUnsafe<RegistryItems>({}),
29
29
  },
30
- ) {}
30
+ );
@@ -1,51 +1,280 @@
1
- import type {
2
- Expand,
3
- IdField,
4
- SystemFields as NonIdSystemFields,
5
- } from "convex/server";
1
+ import type { IdField, SystemFields as NonIdSystemFields } from "convex/server";
2
+ import { pipe } from "effect/Function";
3
+ import * as Array from "effect/Array";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ import * as Predicate from "effect/Predicate";
7
+ import * as Record from "effect/Record";
6
8
  import * as Schema from "effect/Schema";
9
+ import * as SchemaAST from "effect/SchemaAST";
10
+ import * as SchemaGetter from "effect/SchemaGetter";
11
+ import * as SchemaTransformation from "effect/SchemaTransformation";
12
+ import * as Struct from "effect/Struct";
7
13
  import * as GenericId from "./GenericId";
8
14
 
9
- type SystemFieldsSchema<TableName extends string> = Schema.Struct<{
10
- _id: Schema.Schema<
11
- GenericId.GenericId<TableName>,
12
- GenericId.GenericId<TableName>,
13
- never
14
- >;
15
- _creationTime: typeof Schema.Number;
16
- }>;
17
-
18
15
  /**
19
- * Produces a schema for Convex system fields.
16
+ * Produces a schema for Convex system fields. In Confect, system fields include `_id`.
20
17
  */
21
- export const SystemFields = <TableName extends string>(
22
- tableName: TableName,
23
- ): SystemFieldsSchema<TableName> =>
18
+ export const SystemFields = <TableName extends string>(tableName: TableName) =>
24
19
  Schema.Struct({
25
20
  _id: GenericId.GenericId(tableName),
26
21
  _creationTime: Schema.Number,
27
22
  });
28
23
 
24
+ /**
25
+ * The field map added to a table schema, derived from {@link SystemFields} so it
26
+ * stays in lockstep with the runtime schema.
27
+ */
28
+ type SystemFieldsFor<TableName extends string> = ReturnType<
29
+ typeof SystemFields<TableName>
30
+ >["fields"];
31
+
32
+ /**
33
+ * The system field names, derived from {@link SystemFields} so they stay in
34
+ * lockstep with the schema.
35
+ */
36
+ const systemFieldNames: ReadonlyArray<string> = Record.keys(
37
+ SystemFields("").fields,
38
+ );
39
+
40
+ const isSystemFieldKey = (key: string | symbol): key is string =>
41
+ Predicate.isString(key) && Array.contains(systemFieldNames, key);
42
+
43
+ const splitSystemFields = (input: { [key: PropertyKey]: unknown }) => {
44
+ const keys = Reflect.ownKeys(input);
45
+ const pick = (pickedKeys: ReadonlyArray<string | symbol>) =>
46
+ Record.fromEntries(
47
+ Array.map(pickedKeys, (key) => [key, input[key]] as const),
48
+ );
49
+ return {
50
+ system: pick(Array.filter(keys, isSystemFieldKey)),
51
+ rest: pick(Array.filter(keys, Predicate.not(isSystemFieldKey))),
52
+ };
53
+ };
54
+
55
+ /**
56
+ * Wraps a transformation getter so the system fields bypass it: they are split
57
+ * off the input before the user-defined getter runs and merged back into its
58
+ * output. Transformations construct fresh objects, so without this the system
59
+ * fields would be dropped at every transformation step.
60
+ */
61
+ const wrapGetter = (
62
+ getter: SchemaGetter.Getter<any, any, any>,
63
+ ): SchemaGetter.Getter<any, any, any> =>
64
+ new SchemaGetter.Getter((input, options) => {
65
+ if (Option.isNone(input) || !Predicate.isObject(input.value)) {
66
+ return getter.run(input, options);
67
+ }
68
+ const { system, rest } = splitSystemFields(input.value);
69
+ return getter
70
+ .run(Option.some(rest), options)
71
+ .pipe(
72
+ Effect.map(
73
+ Option.map((output) =>
74
+ Predicate.isObject(output) ? Struct.assign(output, system) : output,
75
+ ),
76
+ ),
77
+ );
78
+ });
79
+
80
+ const wrapTransformation = (
81
+ transformation: SchemaAST.Link["transformation"],
82
+ ): SchemaTransformation.Transformation<any, any, any, any> => {
83
+ if (!SchemaTransformation.isTransformation(transformation)) {
84
+ throw new Error(
85
+ "Cannot extend a table schema that uses decoding/encoding middleware with system fields",
86
+ );
87
+ }
88
+ return new SchemaTransformation.Transformation(
89
+ wrapGetter(transformation.decode),
90
+ wrapGetter(transformation.encode),
91
+ );
92
+ };
93
+
94
+ /**
95
+ * Builds an AST walker that adds the given system-field property signatures to
96
+ * every object node of a schema's AST — including each step of its encoding
97
+ * chain — wrapping transformations so the fields survive them.
98
+ */
99
+ const makeExtendAst = (
100
+ systemPropertySignatures: ReadonlyArray<SchemaAST.PropertySignature>,
101
+ ) => {
102
+ const extendEncoding = (encoding: SchemaAST.Encoding): SchemaAST.Encoding =>
103
+ Array.map(
104
+ encoding,
105
+ (link) =>
106
+ new SchemaAST.Link(
107
+ extendAst(link.to),
108
+ wrapTransformation(link.transformation),
109
+ ),
110
+ );
111
+
112
+ const extendAst = (ast: SchemaAST.AST): SchemaAST.AST => {
113
+ if (SchemaAST.isUnion(ast)) {
114
+ return new SchemaAST.Union(
115
+ Array.map(ast.types, extendAst),
116
+ ast.mode,
117
+ ast.annotations,
118
+ ast.checks,
119
+ ast.encoding === undefined ? undefined : extendEncoding(ast.encoding),
120
+ ast.context,
121
+ ast.encodingChecks,
122
+ );
123
+ }
124
+ if (SchemaAST.isSuspend(ast)) {
125
+ return new SchemaAST.Suspend(
126
+ () => extendAst(ast.thunk()),
127
+ ast.annotations,
128
+ undefined,
129
+ ast.encoding === undefined ? undefined : extendEncoding(ast.encoding),
130
+ ast.context,
131
+ );
132
+ }
133
+ if (SchemaAST.isObjects(ast)) {
134
+ return new SchemaAST.Objects(
135
+ Array.appendAll(ast.propertySignatures, systemPropertySignatures),
136
+ ast.indexSignatures,
137
+ ast.annotations,
138
+ ast.checks,
139
+ ast.encoding === undefined ? undefined : extendEncoding(ast.encoding),
140
+ ast.context,
141
+ ast.encodingChecks,
142
+ );
143
+ }
144
+ throw new Error(
145
+ ast._tag === "Declaration"
146
+ ? "Cannot extend a `Declaration` schema (such as a `Schema.Class`) with system fields: its decoded values are constructed instances, which cannot gain extra fields. Use a plain `Schema.Struct`, or transform to one with `Schema.decodeTo`."
147
+ : `Cannot extend a \`${ast._tag}\` schema node with system fields: a table schema must resolve to an object shape at every step of its encoding.`,
148
+ );
149
+ };
150
+
151
+ return extendAst;
152
+ };
153
+
29
154
  /**
30
155
  * Extend a table schema with Convex system fields.
156
+ *
157
+ * A plain `Struct` gains the fields via `Schema.fieldsAssign` and a `Union`
158
+ * has them distributed across its members, preserving the schema's
159
+ * `Struct`/`Union` structure. Any other object-shaped schema — one built with
160
+ * `Schema.decodeTo`/`Schema.encodeKeys`, a branded struct, a suspended schema
161
+ * — is extended at the AST level: every object node in its encoding chain
162
+ * gains the system fields, and each transformation is wrapped so the fields
163
+ * bypass the user-defined getters. Schemas that do not resolve to an object
164
+ * shape at every step (such as `Schema.Class`, whose decoded values are class
165
+ * instances) are rejected with a descriptive error.
166
+ *
167
+ * Note that struct-level checks on an extended schema observe the value with
168
+ * the system fields attached, and encoding an extended schema emits the system
169
+ * fields alongside the table fields.
31
170
  */
32
171
  export const extendWithSystemFields = <
33
172
  TableName extends string,
34
- TableSchema extends Schema.Schema.AnyNoContext,
173
+ TableSchema extends Schema.Codec<any, any>,
35
174
  >(
36
175
  tableName: TableName,
37
176
  schema: TableSchema,
38
- ): ExtendWithSystemFields<TableName, TableSchema> =>
39
- Schema.extend(SystemFields(tableName), schema);
177
+ ): ExtendWithSystemFields<TableName, TableSchema> => {
178
+ const system = SystemFields(tableName).fields;
179
+
180
+ const extendAst = makeExtendAst(
181
+ pipe(
182
+ system,
183
+ Record.toEntries,
184
+ Array.map(
185
+ ([name, field]) => new SchemaAST.PropertySignature(name, field.ast),
186
+ ),
187
+ ),
188
+ );
189
+
190
+ const extend = (s: Schema.Top): Schema.Top => {
191
+ if (
192
+ s.ast.encoding === undefined &&
193
+ SchemaAST.isUnion(s.ast) &&
194
+ Array.isArray(
195
+ (s as Partial<Schema.Union<ReadonlyArray<Schema.Top>>>).members,
196
+ )
197
+ ) {
198
+ return Schema.Union(
199
+ Array.map(
200
+ (s as Schema.Union<ReadonlyArray<Schema.Top>>).members,
201
+ extend,
202
+ ),
203
+ );
204
+ }
205
+ if (
206
+ s.ast.encoding === undefined &&
207
+ SchemaAST.isObjects(s.ast) &&
208
+ typeof (s as Partial<Schema.Struct<Schema.Struct.Fields>>).mapFields ===
209
+ "function"
210
+ ) {
211
+ return Schema.fieldsAssign(system)(
212
+ s as Schema.Struct<Schema.Struct.Fields>,
213
+ );
214
+ }
215
+ return Schema.make(extendAst(s.ast));
216
+ };
217
+
218
+ return extend(schema) as unknown as ExtendWithSystemFields<
219
+ TableName,
220
+ TableSchema
221
+ >;
222
+ };
223
+
224
+ /**
225
+ * Applies the system fields to a single struct, mirroring `Schema.fieldsAssign`.
226
+ * Any other (already-extended or opaque) schema falls back to a `Codec` carrying
227
+ * the system-field document shape.
228
+ */
229
+ type ApplySystemFields<TableName extends string, S> =
230
+ S extends Schema.Struct<infer Fields extends Schema.Struct.Fields>
231
+ ? Schema.Struct<
232
+ Struct.Simplify<Struct.Assign<Fields, SystemFieldsFor<TableName>>>
233
+ >
234
+ : S extends Schema.Codec<infer Type, infer Encoded>
235
+ ? Schema.Codec<
236
+ WithSystemFields<TableName, Type>,
237
+ WithSystemFields<TableName, Encoded>
238
+ >
239
+ : never;
40
240
 
41
241
  /**
42
242
  * Extend a table schema with Convex system fields at the type level.
243
+ *
244
+ * This mirrors the runtime {@link extendWithSystemFields}: a `Struct` gains the
245
+ * system fields directly (matching `Schema.fieldsAssign`), and a `Union` has
246
+ * them distributed across its members (matching
247
+ * `union.mapMembers(Tuple.map(Schema.fieldsAssign(...)))`). The `Struct`/`Union`
248
+ * structure is preserved rather than collapsed to a bare `Codec`; any other
249
+ * schema falls back to a `Codec` carrying the system-field document shape,
250
+ * matching the runtime's AST-level extension.
43
251
  */
44
252
  export type ExtendWithSystemFields<
45
253
  TableName extends string,
46
- TableSchema extends Schema.Schema.AnyNoContext,
47
- > = Schema.extend<SystemFieldsSchema<TableName>, TableSchema>;
254
+ TableSchema extends Schema.Top,
255
+ > =
256
+ TableSchema extends Schema.Union<
257
+ infer Members extends ReadonlyArray<Schema.Top>
258
+ >
259
+ ? Schema.Union<
260
+ Struct.Simplify<
261
+ Readonly<{
262
+ [K in keyof Members]: ExtendWithSystemFields<TableName, Members[K]>;
263
+ }>
264
+ >
265
+ >
266
+ : ApplySystemFields<TableName, TableSchema>;
48
267
 
49
- export type WithSystemFields<TableName extends string, Document> = Expand<
50
- Readonly<IdField<TableName>> & Readonly<NonIdSystemFields> & Document
51
- >;
268
+ /**
269
+ * The decoded/encoded document shape: a table's fields plus Convex's system
270
+ * fields. Deliberately a bare intersection (no `Expand`/`Simplify`) — flattening
271
+ * it with a homomorphic mapped type collapses to `{ [x: string]: any }` when
272
+ * `Document` is still an unresolved generic, which breaks structural
273
+ * comparability in the database reader/writer plumbing.
274
+ */
275
+ export type WithSystemFields<
276
+ TableName extends string,
277
+ Document,
278
+ > = Document extends unknown
279
+ ? IdField<TableName> & NonIdSystemFields & Document
280
+ : never;