@confect/core 10.0.0-next.21 → 10.0.0-next.23

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 (82) hide show
  1. package/CHANGELOG.md +168 -81
  2. package/dist/FunctionProvenance.d.ts +34 -26
  3. package/dist/FunctionProvenance.d.ts.map +1 -1
  4. package/dist/FunctionProvenance.js +14 -13
  5. package/dist/FunctionProvenance.js.map +1 -1
  6. package/dist/FunctionSpec.d.ts +15 -9
  7. package/dist/FunctionSpec.d.ts.map +1 -1
  8. package/dist/FunctionSpec.js +12 -6
  9. package/dist/FunctionSpec.js.map +1 -1
  10. package/dist/GroupPath.d.ts +5 -6
  11. package/dist/GroupPath.d.ts.map +1 -1
  12. package/dist/GroupPath.js.map +1 -1
  13. package/dist/GroupSpec.d.ts +10 -3
  14. package/dist/GroupSpec.d.ts.map +1 -1
  15. package/dist/GroupSpec.js +33 -19
  16. package/dist/GroupSpec.js.map +1 -1
  17. package/dist/Identifier.d.ts +6 -5
  18. package/dist/Identifier.d.ts.map +1 -1
  19. package/dist/Identifier.js +6 -5
  20. package/dist/Identifier.js.map +1 -1
  21. package/dist/Lazy.d.ts +11 -13
  22. package/dist/Lazy.d.ts.map +1 -1
  23. package/dist/Lazy.js +11 -13
  24. package/dist/Lazy.js.map +1 -1
  25. package/dist/MiddlewareAttachment.d.ts +171 -0
  26. package/dist/MiddlewareAttachment.d.ts.map +1 -0
  27. package/dist/MiddlewareAttachment.js +48 -0
  28. package/dist/MiddlewareAttachment.js.map +1 -0
  29. package/dist/MiddlewareSpec.d.ts +59 -45
  30. package/dist/MiddlewareSpec.d.ts.map +1 -1
  31. package/dist/MiddlewareSpec.js +9 -4
  32. package/dist/MiddlewareSpec.js.map +1 -1
  33. package/dist/PaginationError.d.ts +9 -3
  34. package/dist/PaginationError.d.ts.map +1 -1
  35. package/dist/PaginationError.js +9 -3
  36. package/dist/PaginationError.js.map +1 -1
  37. package/dist/PaginationOptions.d.ts +3 -3
  38. package/dist/PaginationOptions.js +3 -3
  39. package/dist/PaginationOptions.js.map +1 -1
  40. package/dist/Ref.d.ts +42 -35
  41. package/dist/Ref.d.ts.map +1 -1
  42. package/dist/Ref.js +32 -30
  43. package/dist/Ref.js.map +1 -1
  44. package/dist/Refs.d.ts +1 -1
  45. package/dist/Refs.d.ts.map +1 -1
  46. package/dist/Refs.js +3 -2
  47. package/dist/Refs.js.map +1 -1
  48. package/dist/SchemaToValidator.d.ts.map +1 -1
  49. package/dist/SchemaToValidator.js +4 -2
  50. package/dist/SchemaToValidator.js.map +1 -1
  51. package/dist/Spec.d.ts +15 -12
  52. package/dist/Spec.d.ts.map +1 -1
  53. package/dist/Spec.js +13 -8
  54. package/dist/Spec.js.map +1 -1
  55. package/dist/SystemFields.d.ts +15 -14
  56. package/dist/SystemFields.d.ts.map +1 -1
  57. package/dist/SystemFields.js +11 -10
  58. package/dist/SystemFields.js.map +1 -1
  59. package/dist/Table.d.ts +1 -0
  60. package/dist/Table.d.ts.map +1 -1
  61. package/dist/Table.js.map +1 -1
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +3 -2
  65. package/package.json +8 -7
  66. package/src/FunctionProvenance.ts +35 -27
  67. package/src/FunctionSpec.ts +33 -15
  68. package/src/GroupPath.ts +5 -6
  69. package/src/GroupSpec.ts +83 -31
  70. package/src/Identifier.ts +6 -5
  71. package/src/Lazy.ts +11 -13
  72. package/src/MiddlewareAttachment.ts +106 -0
  73. package/src/MiddlewareSpec.ts +79 -43
  74. package/src/PaginationError.ts +9 -3
  75. package/src/PaginationOptions.ts +3 -3
  76. package/src/Ref.ts +76 -65
  77. package/src/Refs.ts +3 -3
  78. package/src/SchemaToValidator.ts +53 -44
  79. package/src/Spec.ts +50 -39
  80. package/src/SystemFields.ts +18 -17
  81. package/src/Table.ts +3 -2
  82. package/src/index.ts +1 -0
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@confect/core",
3
3
  "description": "Shared specs and schemas used by all Confect packages",
4
- "version": "10.0.0-next.21",
4
+ "version": "10.0.0-next.23",
5
5
  "author": "RJ Dellecese",
6
6
  "bugs": {
7
7
  "url": "https://github.com/rjdellecese/confect/issues"
8
8
  },
9
9
  "devDependencies": {
10
- "@effect/vitest": "4.0.0-rc.112",
10
+ "@effect/vitest": "4.0.0-rc.115",
11
11
  "@types/node": "24.13.3",
12
12
  "confect-bench-harness": "0.0.0",
13
- "effect": "4.0.0-rc.112",
14
- "tsdown": "0.22.14",
13
+ "effect": "4.0.0-rc.115",
14
+ "tsdown": "0.23.0",
15
15
  "tsx": "4.23.13",
16
16
  "typescript": "7.0.2",
17
17
  "unrun": "0.3.1",
18
- "vitest": "4.1.11"
18
+ "vitest": "5.0.0"
19
19
  },
20
20
  "engines": {
21
21
  "node": ">=24"
@@ -51,7 +51,7 @@
51
51
  "module": "./dist/index.js",
52
52
  "peerDependencies": {
53
53
  "convex": "^1.32.0",
54
- "effect": "^4.0.0-rc.112"
54
+ "effect": "^4.0.0-rc.115"
55
55
  },
56
56
  "repository": {
57
57
  "type": "git",
@@ -65,6 +65,7 @@
65
65
  "clean": "shx rm -rf dist coverage node_modules",
66
66
  "dev": "tsdown --watch --no-clean --config-loader unrun",
67
67
  "test": "vitest run",
68
- "typecheck": "tsc -b tsconfig.src.json"
68
+ "typecheck": "tsc -b tsconfig.src.json",
69
+ "typecheck:inexact": "tsc --noEmit --project tsconfig.json --exactOptionalPropertyTypes false"
69
70
  }
70
71
  }
@@ -27,7 +27,9 @@ export interface ArgsSchema<
27
27
  readonly fields: ArgsFields_;
28
28
  }
29
29
 
30
- /** Erased structural view used after the exact field map is no longer needed. */
30
+ /**
31
+ * Erased structural view used after the exact field map is no longer needed.
32
+ */
31
33
  export type AnyArgsSchema = ArgsSchema<ArgsFields>;
32
34
 
33
35
  export type FunctionProvenance = Data.TaggedEnum<{
@@ -44,12 +46,11 @@ export type FunctionProvenance = Data.TaggedEnum<{
44
46
  }>;
45
47
 
46
48
  /**
47
- * The declaration shape of a Confect function — orthogonal to both the
48
- * provenance origin (Confect vs Convex) and the function type
49
- * (query/mutation/action). A `Standard` function declares its args fields and
50
- * returns schema directly; a `Paginated` function declares user-args fields
51
- * and an item schema from which the Convex-facing `args`/`returns` are
52
- * composed.
49
+ * The declaration shape of a Confect function—orthogonal to both the provenance
50
+ * origin (Confect vs Convex) and the function type (query/mutation/action). A
51
+ * `Standard` function declares its args fields and returns schema directly; a
52
+ * `Paginated` function declares user-args fields and an item schema from which
53
+ * the Convex-facing `args`/`returns` are composed.
53
54
  */
54
55
  export type ConfectKind = Standard | Paginated;
55
56
 
@@ -62,11 +63,17 @@ export interface Paginated<
62
63
  Item extends Schema.Codec<any, any> = Schema.Codec<any, any>,
63
64
  > {
64
65
  readonly _tag: "Paginated";
65
- /** User-declared args — no `paginationOpts`. */
66
+ /**
67
+ * User-declared args—no `paginationOpts`.
68
+ */
66
69
  readonly userArgs: ArgsSchema<UserArgsFields_>;
67
- /** Page element schema. */
70
+ /**
71
+ * Page element schema.
72
+ */
68
73
  readonly item: Item;
69
- /** Mutable array of items — the page decode target. */
74
+ /**
75
+ * Mutable array of items—the page decode target.
76
+ */
70
77
  readonly page: Schema.Codec<any, any>;
71
78
  }
72
79
 
@@ -105,19 +112,20 @@ export const FunctionProvenance = Data.taggedEnum<FunctionProvenance>();
105
112
  const Standard: Standard = { _tag: "Standard" };
106
113
 
107
114
  /**
108
- * Build a `Confect` provenance from lazy args-fields and schema thunks.
109
- * `args`, `returns`, and `error` are exposed as sync lazy memoised getters
110
- * (via {@link Lazy.defineProperty}) that only evaluate their thunk on first
111
- * access, mirroring how `Table` defers `Fields`/`Doc`. This keeps importing the
112
- * assembled `_generated/spec.ts` cheap — no `Schema.Struct(...)` /
113
- * `Schema.Array(...)` work runs at module load; it is deferred to the first
114
- * invocation that actually compiles validators or runs a codec.
115
+ * Build a `Confect` provenance from lazy args-fields and schema thunks. `args`,
116
+ * `returns`, and `error` are exposed as sync lazy memoised getters (via
117
+ * {@link Lazy.defineProperty}) that only evaluate their thunk on first access,
118
+ * mirroring how `Table` defers `Fields`/`Doc`. This keeps importing the
119
+ * assembled `_generated/spec.ts` cheap—no
120
+ * `Schema.Struct(...)`/`Schema.Array(...)` work runs at module load; it is
121
+ * deferred to the first invocation that actually compiles validators or runs a
122
+ * codec.
115
123
  *
116
124
  * The object is built by hand rather than through `FunctionProvenance.Confect`
117
125
  * because the `Data` constructor copies its input with `Object.assign`, which
118
- * would force the getters at construction time and defeat the laziness.
119
- * `error` is only installed when an `errorThunk` is provided, so its absence
120
- * is observable via `"error" in provenance` without forcing anything; nothing
126
+ * would force the getters at construction time and defeat the laziness. `error`
127
+ * is only installed when an `errorThunk` is provided, so its absence is
128
+ * observable via `"error" in provenance` without forcing anything; nothing
121
129
  * relies on `Data`'s structural `Equal`/`Hash` for provenance values.
122
130
  */
123
131
  export const Confect = <
@@ -141,8 +149,8 @@ export const Confect = <
141
149
  };
142
150
 
143
151
  /**
144
- * The composed args schema of a paginated query: the user-declared fields
145
- * plus the `paginationOpts` field managed by Convex's pagination protocol.
152
+ * The composed args schema of a paginated query: the user-declared fields plus
153
+ * the `paginationOpts` field managed by Convex's pagination protocol.
146
154
  *
147
155
  * The fields-first representation keeps the user-declared map available for
148
156
  * type-level composition without reconstructing it from an erased schema.
@@ -157,8 +165,8 @@ export type PaginatedArgs<UserArgsFields extends ArgsFields> = Schema.Struct<
157
165
  >;
158
166
 
159
167
  /**
160
- * The composed returns schema of a paginated query: a `PaginationResult` of
161
- * the item schema. Same deferred-conditional device as {@link PaginatedArgs}.
168
+ * The composed returns schema of a paginated query: a `PaginationResult` of the
169
+ * item schema. Same deferred-conditional device as {@link PaginatedArgs}.
162
170
  */
163
171
  export type PaginatedReturns<Item extends Schema.Codec<any, any>> =
164
172
  PaginationResult.PaginationResult<Item> extends infer ComposedReturns extends
@@ -183,8 +191,8 @@ export interface AnyConfectPaginated extends AnyConfect {
183
191
 
184
192
  /**
185
193
  * Build the provenance of a paginated query from lazy schema thunks, with the
186
- * same laziness contract as {@link Confect}. The user-facing schemas live on
187
- * the `Paginated` kind (`kind.userArgs`, `kind.item`, `kind.page`) — the kind
194
+ * same laziness contract as {@link Confect}. The user-facing schemas live on the
195
+ * `Paginated` kind (`kind.userArgs`, `kind.item`, `kind.page`)—the kind
188
196
  * container is built eagerly (it is cheap) while the schemas inside it stay
189
197
  * lazy. The composed Convex-facing `args`/`returns` are derived lazily from
190
198
  * them: `args` spreads the user fields plus `paginationOpts`, and `returns`
@@ -216,7 +224,7 @@ export const ConfectPaginated = <
216
224
  const fields = paginatedKind.userArgs.fields;
217
225
  if ("paginationOpts" in fields) {
218
226
  throw new globalThis.Error(
219
- "A paginated query's args schema must not declare `paginationOpts` — " +
227
+ "A paginated query's args schema must not declare `paginationOpts`—" +
220
228
  "it is added automatically from the `PaginationOptions` schema",
221
229
  );
222
230
  }
@@ -1,3 +1,4 @@
1
+ import type * as MiddlewareAttachment from "./MiddlewareAttachment";
1
2
  import type {
2
3
  DefaultFunctionArgs,
3
4
  FunctionType,
@@ -32,6 +33,9 @@ export interface FunctionSpec<
32
33
  readonly name: Name_;
33
34
  readonly functionProvenance: FunctionProvenance_;
34
35
  readonly middlewareSpecs: ReadonlyArray<MiddlewareSpecs_>;
36
+ readonly middlewareAttachments: ReadonlyArray<
37
+ MiddlewareAttachment.MiddlewareAttachment<MiddlewareSpecs_>
38
+ >;
35
39
  }
36
40
 
37
41
  export interface Builder<
@@ -55,6 +59,7 @@ export interface Builder<
55
59
  FunctionProvenance_,
56
60
  MiddlewareSpecs_
57
61
  >,
62
+ ...options: MiddlewareAttachment.Args<NoInfer<MiddlewareSpec_>>
58
63
  ): Builder<
59
64
  RuntimeAndFunctionType_,
60
65
  FunctionVisibility_,
@@ -136,7 +141,9 @@ export type Name<FunctionSpec_ extends AnyWithProps> = FunctionSpec_["name"];
136
141
  export type MiddlewareSpecs<FunctionSpec_ extends AnyWithProps> =
137
142
  FunctionSpec_["middlewareSpecs"][number];
138
143
 
139
- /** The field map declared by a Confect-provenance spec. */
144
+ /**
145
+ * The field map declared by a Confect-provenance spec.
146
+ */
140
147
  export type ArgsFields<FunctionSpec_ extends AnyWithProps> =
141
148
  FunctionSpec_ extends {
142
149
  readonly functionProvenance: {
@@ -152,7 +159,9 @@ export type ArgsFields<FunctionSpec_ extends AnyWithProps> =
152
159
  ? FunctionProvenance.ArgsFields
153
160
  : never;
154
161
 
155
- /** The args schema assembled from a Confect-provenance spec's field map. */
162
+ /**
163
+ * The args schema assembled from a Confect-provenance spec's field map.
164
+ */
156
165
  export type ArgsSchema<FunctionSpec_ extends AnyWithProps> =
157
166
  FunctionSpec_ extends {
158
167
  readonly functionProvenance: {
@@ -186,10 +195,10 @@ export type ErrorSchema<FunctionSpec_ extends AnyWithProps> =
186
195
  FunctionSpec_ extends {
187
196
  readonly functionProvenance: {
188
197
  readonly _tag: "Confect";
189
- readonly error?: infer ErrorSchema_ extends Schema.Codec<any, any>;
198
+ readonly error?: infer ErrorSchema_;
190
199
  };
191
200
  }
192
- ? ErrorSchema_
201
+ ? Extract<ErrorSchema_, Schema.Codec<any, any>>
193
202
  : never;
194
203
 
195
204
  export type Args<FunctionSpec_ extends AnyWithProps> = FunctionSpec_ extends {
@@ -282,9 +291,14 @@ export type WithoutName<
282
291
  const Proto = {
283
292
  [TypeId]: TypeId,
284
293
 
294
+ get middlewareSpecs() {
295
+ return this.middlewareAttachments.map(({ spec }) => spec);
296
+ },
297
+
285
298
  middleware(
286
299
  this: AnyWithProps,
287
300
  middlewareSpec: MiddlewareSpec.AnyMiddlewareSpec,
301
+ ...options: ReadonlyArray<unknown>
288
302
  ) {
289
303
  if (this.functionProvenance._tag === "Convex") {
290
304
  throw new Error(
@@ -298,8 +312,9 @@ const Proto = {
298
312
  );
299
313
  }
300
314
  if (
301
- this.middlewareSpecs.some(
302
- (existing) => existing.key === middlewareSpec.key,
315
+ !("options" in middlewareSpec) &&
316
+ this.middlewareAttachments.some(
317
+ ({ spec }) => spec.key === middlewareSpec.key,
303
318
  )
304
319
  ) {
305
320
  throw new Error(
@@ -312,10 +327,13 @@ const Proto = {
312
327
  functionVisibility: this.functionVisibility,
313
328
  name: this.name,
314
329
  functionProvenance: this.functionProvenance,
315
- middlewareSpecs: [...this.middlewareSpecs, middlewareSpec],
330
+ middlewareAttachments: [
331
+ ...this.middlewareAttachments,
332
+ { spec: middlewareSpec, options: options[0] },
333
+ ],
316
334
  });
317
335
  },
318
- };
336
+ } satisfies ThisType<AnyWithProps>;
319
337
 
320
338
  interface Options {
321
339
  readonly name: string;
@@ -373,7 +391,7 @@ const make = <
373
391
  args === undefined
374
392
  ? FunctionProvenance.Confect(() => ({}), returns, error)
375
393
  : FunctionProvenance.Confect(args, returns, error),
376
- middlewareSpecs: [],
394
+ middlewareAttachments: [],
377
395
  });
378
396
  }
379
397
 
@@ -381,10 +399,10 @@ const make = <
381
399
  };
382
400
 
383
401
  /**
384
- * `Schema.Struct` fields that must not declare `paginationOpts`. Used to
385
- * reject a user args schema that redeclares the field the paginated
386
- * constructors add themselves; the check reports at the `args` thunk with an
387
- * unsatisfiable type when violated.
402
+ * `Schema.Struct` fields that must not declare `paginationOpts`. Used to reject
403
+ * a user args schema that redeclares the field the paginated constructors add
404
+ * themselves; the check reports at the `args` thunk with an unsatisfiable type
405
+ * when violated.
388
406
  */
389
407
  type ForbidPaginationOpts<
390
408
  UserArgsFields extends FunctionProvenance.ArgsFields,
@@ -448,7 +466,7 @@ const makePaginated = <
448
466
  args === undefined
449
467
  ? FunctionProvenance.ConfectPaginated(() => ({}), item, error)
450
468
  : FunctionProvenance.ConfectPaginated(args, item, error),
451
- middlewareSpecs: [],
469
+ middlewareAttachments: [],
452
470
  });
453
471
  }
454
472
 
@@ -562,7 +580,7 @@ const makeConvex =
562
580
  ExtractArgs<F>,
563
581
  ExtractReturns<F>
564
582
  >(),
565
- middlewareSpecs: [],
583
+ middlewareAttachments: [],
566
584
  }) as any;
567
585
  };
568
586
 
package/src/GroupPath.ts CHANGED
@@ -4,7 +4,8 @@ import type * as GroupSpec from "./GroupSpec";
4
4
  * Recursively generates paths for a group and its nested groups.
5
5
  *
6
6
  * - For a group with no subgroups, returns just the group name.
7
- * - For a group with subgroups, returns the group name plus all possible paths through its direct subgroups.
7
+ * - For a group with subgroups, returns the group name plus all possible paths
8
+ * through its direct subgroups.
8
9
  */
9
10
  export type All<
10
11
  Group extends GroupSpec.AnyWithProps,
@@ -26,13 +27,11 @@ type AllHelper<
26
27
  : never;
27
28
 
28
29
  /**
29
- * Recursively extracts the group at the given dot-separated path.
30
- * Path must match the format defined in `Path` above, e.g. "group" or "group.subgroup".
30
+ * Recursively extracts the group at the given dot-separated path. Path must
31
+ * match the format defined in `Path` above, e.g. "group" or "group.subgroup".
31
32
  *
32
33
  * @example
33
- * ```ts
34
- * type G = WithPath<RootGroup, "group.subgroup">;
35
- * ```
34
+ * type G = WithPath<RootGroup, "group.subgroup">;
36
35
  */
37
36
  export type GroupAt<
38
37
  Group,
package/src/GroupSpec.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import * as MiddlewareAttachment from "./MiddlewareAttachment";
1
2
  import * as Predicate from "effect/Predicate";
2
3
  import * as Record from "effect/Record";
4
+ import * as Result from "effect/Result";
3
5
  import type * as FunctionSpec from "./FunctionSpec";
4
6
  import type * as MiddlewareSpec from "./MiddlewareSpec";
5
7
  import type * as RuntimeAndFunctionType from "./RuntimeAndFunctionType";
@@ -18,7 +20,7 @@ export interface GroupSpec<
18
20
  // Subgroups may be of any runtime, independent of this group's own runtime: a
19
21
  // group is only a namespace for its children, which are otherwise-independent
20
22
  // modules. Functions, by contrast, stay homogeneous (a Node group only accepts
21
- // Node actions) — `addFunction` keeps the `<Runtime>` bound below.
23
+ // Node actions)—`addFunction` keeps the `<Runtime>` bound below.
22
24
  Groups_ extends AnyWithProps = never,
23
25
  MiddlewareSpecs_ extends MiddlewareSpec.AnyMiddlewareSpec = never,
24
26
  > {
@@ -36,6 +38,9 @@ export interface GroupSpec<
36
38
  [GroupName in Name<Groups_>]: WithName<Groups_, GroupName>;
37
39
  };
38
40
  readonly middlewareSpecs: ReadonlyArray<MiddlewareSpecs_>;
41
+ readonly middlewareAttachments: ReadonlyArray<
42
+ MiddlewareAttachment.MiddlewareAttachment<MiddlewareSpecs_>
43
+ >;
39
44
  readonly "~Functions": Functions_;
40
45
  readonly "~Groups": Groups_;
41
46
 
@@ -72,6 +77,7 @@ export interface GroupSpec<
72
77
  Functions_,
73
78
  MiddlewareSpecs_
74
79
  >,
80
+ ...options: MiddlewareAttachment.Args<NoInfer<MiddlewareSpec_>>
75
81
  ): GroupSpec<
76
82
  Runtime,
77
83
  Name_,
@@ -123,7 +129,10 @@ export type WithName<
123
129
  Name_ extends Name<Group>,
124
130
  > = Extract<Group, { readonly name: Name_ }>;
125
131
 
126
- /** Assigns a segment name to a leaf group created with {@link make} for typing and refs. */
132
+ /**
133
+ * Assigns a segment name to a leaf group created with {@link make} for typing
134
+ * and refs.
135
+ */
127
136
  export type NamedAt<Group extends Any, Name_ extends string> = Omit<
128
137
  Group,
129
138
  "name"
@@ -154,22 +163,27 @@ export type AddGroups<
154
163
  const Proto = {
155
164
  [TypeId]: TypeId,
156
165
 
166
+ get middlewareSpecs() {
167
+ return this.middlewareAttachments.map(({ spec }) => spec);
168
+ },
169
+
157
170
  addFunction<Function extends FunctionSpec.AnyWithProps>(
158
171
  this: Any,
159
172
  function_: Function,
160
173
  ) {
161
174
  const this_ = this as AnyWithProps;
162
175
 
163
- const overlapping = function_.middlewareSpecs.find(
164
- (functionMiddlewareSpec) =>
165
- this_.middlewareSpecs.some(
166
- (groupMiddlewareSpec) =>
176
+ const overlapping = function_.middlewareAttachments.find(
177
+ ({ spec: functionMiddlewareSpec }) =>
178
+ !("options" in functionMiddlewareSpec) &&
179
+ this_.middlewareAttachments.some(
180
+ ({ spec: groupMiddlewareSpec }) =>
167
181
  groupMiddlewareSpec.key === functionMiddlewareSpec.key,
168
182
  ),
169
183
  );
170
184
  if (overlapping !== undefined) {
171
185
  throw new Error(
172
- `Middleware "${overlapping.key}" is attached to both function "${function_.name}" and its group`,
186
+ `Middleware "${overlapping.spec.key}" is attached to both function "${function_.name}" and its group`,
173
187
  );
174
188
  }
175
189
 
@@ -178,7 +192,7 @@ const Proto = {
178
192
  name: this_.name,
179
193
  functions: Record.set(this_.functions, function_.name, function_),
180
194
  groups: this_.groups,
181
- middlewareSpecs: this_.middlewareSpecs,
195
+ middlewareAttachments: this_.middlewareAttachments,
182
196
  });
183
197
  },
184
198
 
@@ -191,7 +205,7 @@ const Proto = {
191
205
  name: this_.name,
192
206
  functions: this_.functions,
193
207
  groups: Record.set(this_.groups, group_.name, group_),
194
- middlewareSpecs: this_.middlewareSpecs,
208
+ middlewareAttachments: this_.middlewareAttachments,
195
209
  });
196
210
  },
197
211
 
@@ -204,19 +218,21 @@ const Proto = {
204
218
  name: this_.name,
205
219
  functions: this_.functions,
206
220
  groups: Record.set(this_.groups, name, withName(name, group_)),
207
- middlewareSpecs: this_.middlewareSpecs,
221
+ middlewareAttachments: this_.middlewareAttachments,
208
222
  });
209
223
  },
210
224
 
211
225
  middleware<MiddlewareSpec_ extends MiddlewareSpec.AnyMiddlewareSpec>(
212
226
  this: Any,
213
227
  middlewareSpec: MiddlewareSpec_,
228
+ ...options: ReadonlyArray<unknown>
214
229
  ) {
215
230
  const this_ = this as AnyWithProps;
216
231
 
217
232
  if (
218
- this_.middlewareSpecs.some(
219
- (existing) => existing.key === middlewareSpec.key,
233
+ !("options" in middlewareSpec) &&
234
+ this_.middlewareAttachments.some(
235
+ ({ spec }) => spec.key === middlewareSpec.key,
220
236
  )
221
237
  ) {
222
238
  throw new Error(
@@ -226,8 +242,9 @@ const Proto = {
226
242
 
227
243
  for (const function_ of Object.values(this_.functions)) {
228
244
  if (
229
- function_.middlewareSpecs.some(
230
- (existing) => existing.key === middlewareSpec.key,
245
+ !("options" in middlewareSpec) &&
246
+ function_.middlewareAttachments.some(
247
+ ({ spec }) => spec.key === middlewareSpec.key,
231
248
  )
232
249
  ) {
233
250
  throw new Error(
@@ -241,37 +258,53 @@ const Proto = {
241
258
  name: this_.name,
242
259
  functions: this_.functions,
243
260
  groups: this_.groups,
244
- middlewareSpecs: [...this_.middlewareSpecs, middlewareSpec],
261
+ middlewareAttachments: [
262
+ ...this_.middlewareAttachments,
263
+ { spec: middlewareSpec, options: options[0] },
264
+ ],
245
265
  });
246
266
  },
247
- };
267
+ } satisfies ThisType<AnyWithProps>;
248
268
 
249
269
  const makeProto = <
250
270
  Runtime extends RuntimeAndFunctionType.Runtime,
251
271
  Name_ extends string,
252
272
  Functions_ extends FunctionSpec.AnyWithPropsWithRuntime<Runtime>,
253
273
  Groups_ extends AnyWithPropsWithRuntime<Runtime>,
254
- MiddlewareSpecs_ extends MiddlewareSpec.AnyMiddlewareSpec,
274
+ MiddlewareAttachments_ extends
275
+ ReadonlyArray<MiddlewareAttachment.MiddlewareAttachment>,
255
276
  >({
256
277
  runtime,
257
278
  name,
258
279
  functions,
259
280
  groups,
260
- middlewareSpecs,
281
+ middlewareAttachments,
261
282
  }: {
262
283
  runtime: Runtime;
263
284
  name: Name_;
264
285
  functions: Record.ReadonlyRecord<string, Functions_>;
265
286
  groups: Record.ReadonlyRecord<string, Groups_>;
266
- middlewareSpecs: ReadonlyArray<MiddlewareSpecs_>;
267
- }): GroupSpec<Runtime, Name_, Functions_, Groups_, MiddlewareSpecs_> =>
287
+ middlewareAttachments: MiddlewareAttachments_;
288
+ }): GroupSpec<
289
+ Runtime,
290
+ Name_,
291
+ Functions_,
292
+ Groups_,
293
+ MiddlewareAttachments_[number]["spec"]
294
+ > =>
268
295
  Object.assign(Object.create(Proto), {
269
296
  runtime,
270
297
  name,
271
298
  functions,
272
299
  groups,
273
- middlewareSpecs,
274
- }) as GroupSpec<Runtime, Name_, Functions_, Groups_, MiddlewareSpecs_>;
300
+ middlewareAttachments,
301
+ }) as GroupSpec<
302
+ Runtime,
303
+ Name_,
304
+ Functions_,
305
+ Groups_,
306
+ MiddlewareAttachments_[number]["spec"]
307
+ >;
275
308
 
276
309
  export const make = (): GroupSpec<"Convex", ""> =>
277
310
  makeProto({
@@ -279,7 +312,7 @@ export const make = (): GroupSpec<"Convex", ""> =>
279
312
  name: "",
280
313
  functions: Record.empty(),
281
314
  groups: Record.empty(),
282
- middlewareSpecs: [],
315
+ middlewareAttachments: [],
283
316
  });
284
317
 
285
318
  export const makeAt = <const Name_ extends string>(
@@ -292,7 +325,7 @@ export const makeAt = <const Name_ extends string>(
292
325
  name,
293
326
  functions: Record.empty(),
294
327
  groups: Record.empty(),
295
- middlewareSpecs: [],
328
+ middlewareAttachments: [],
296
329
  });
297
330
  };
298
331
 
@@ -302,7 +335,7 @@ export const makeNode = (): GroupSpec<"Node", ""> =>
302
335
  name: "",
303
336
  functions: Record.empty(),
304
337
  groups: Record.empty(),
305
- middlewareSpecs: [],
338
+ middlewareAttachments: [],
306
339
  });
307
340
 
308
341
  export const makeNodeAt = <const Name_ extends string>(
@@ -315,14 +348,15 @@ export const makeNodeAt = <const Name_ extends string>(
315
348
  name,
316
349
  functions: Record.empty(),
317
350
  groups: Record.empty(),
318
- middlewareSpecs: [],
351
+ middlewareAttachments: [],
319
352
  });
320
353
  };
321
354
 
322
- export const withName = <const Name_ extends string>(
355
+ export function withName<const Name_ extends string, Group extends Any>(
323
356
  name: Name_,
324
- group: Any,
325
- ): AnyWithProps => {
357
+ group: Group,
358
+ ): NamedAt<Group, Name_>;
359
+ export function withName(name: string, group: Any): AnyWithProps {
326
360
  validateConfectFunctionIdentifier(name);
327
361
  const group_ = group as AnyWithProps;
328
362
 
@@ -335,6 +369,24 @@ export const withName = <const Name_ extends string>(
335
369
  name,
336
370
  functions: group_.functions,
337
371
  groups: group_.groups,
338
- middlewareSpecs: group_.middlewareSpecs,
372
+ middlewareAttachments: group_.middlewareAttachments,
373
+ });
374
+ }
375
+
376
+ export const validateMiddleware = (
377
+ group: AnyWithProps,
378
+ ): Result.Result<void, MiddlewareAttachment.ValidationError> =>
379
+ Result.gen(function* () {
380
+ yield* MiddlewareAttachment.validateAll(
381
+ group.middlewareAttachments,
382
+ `group "${group.name}"`,
383
+ );
384
+ for (const function_ of Object.values(group.functions)) {
385
+ yield* MiddlewareAttachment.validateAll(
386
+ [...group.middlewareAttachments, ...function_.middlewareAttachments],
387
+ `function "${function_.name}"`,
388
+ );
389
+ }
390
+ for (const child of Object.values(group.groups))
391
+ yield* validateMiddleware(child);
339
392
  });
340
- };
package/src/Identifier.ts CHANGED
@@ -95,15 +95,16 @@ export const validateConfectFunctionIdentifier = (identifier: string) => {
95
95
  };
96
96
 
97
97
  /**
98
- * Validate that `identifier` is suitable as a Convex table name (and, equivalently,
99
- * as a `confect/tables/<identifier>.ts` filename).
98
+ * Validate that `identifier` is suitable as a Convex table name (and,
99
+ * equivalently, as a `confect/tables/<identifier>.ts` filename).
100
100
  *
101
101
  * Rules:
102
- * - Must match `/^[A-Za-z][A-Za-z0-9_]*$/` — letter-leading, alphanumeric plus
102
+ *
103
+ * - Must match `/^[A-Za-z][A-Za-z0-9_]*$/`—letter-leading, alphanumeric plus
103
104
  * underscore. No `$` (not a valid Convex table name character); no leading
104
105
  * `_` (Convex reserves `_<name>` for its system tables).
105
- * - Must not be a reserved JavaScript identifier, so the name can also be used
106
- * as a binding name in generated code without escaping.
106
+ * - Must not be a reserved JavaScript identifier, so the name can also be used as
107
+ * a binding name in generated code without escaping.
107
108
  */
108
109
  export const validateConfectTableIdentifier = (identifier: string) => {
109
110
  if (!matchesTableNamePattern(identifier)) {
package/src/Lazy.ts CHANGED
@@ -1,21 +1,19 @@
1
1
  /**
2
2
  * Install a lazy memoised property on `target`. The first access runs
3
- * `compute()` and replaces the getter with a plain, non-writable data
4
- * property whose value is the computed result. Subsequent accesses hit
5
- * the V8 fast path for own data properties — no function call, identical
6
- * returned reference — so first and second-and-subsequent accesses are
7
- * observably indistinguishable.
3
+ * `compute()` and replaces the getter with a plain, non-writable data property
4
+ * whose value is the computed result. Subsequent accesses hit the V8 fast path
5
+ * for own data properties—no function call, identical returned reference—so
6
+ * first and second-and-subsequent accesses are observably indistinguishable.
8
7
  *
9
- * The replacement property is `enumerable: true` so the lazy property
10
- * still participates in `Object.keys` / `JSON.stringify` after it
11
- * materialises, matching the shape of a plain data property. The property
12
- * is also `enumerable` before materialising, so presence checks
13
- * (`"key" in target`, `Object.hasOwn(target, key)`) observe it without
14
- * forcing the computation.
8
+ * The replacement property is `enumerable: true` so the lazy property still
9
+ * participates in `Object.keys`/`JSON.stringify` after it materialises,
10
+ * matching the shape of a plain data property. The property is also
11
+ * `enumerable` before materialising, so presence checks (`"key" in target`,
12
+ * `Object.hasOwn(target, key)`) observe it without forcing the computation.
15
13
  *
16
14
  * This is the single shared implementation consumed across packages (e.g.
17
- * `@confect/core`'s lazy `FunctionSpec` schemas and lazy `Table` `Fields` /
18
- * `Doc`), so there is no chance of the two drifting apart.
15
+ * `@confect/core`'s lazy `FunctionSpec` schemas and lazy `Table`
16
+ * `Fields`/`Doc`), so there is no chance of the two drifting apart.
19
17
  */
20
18
  export const defineProperty = <T extends object, K extends PropertyKey>(
21
19
  target: T,