@confect/core 9.0.0-next.5 → 9.0.0-next.6

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.
@@ -56,6 +56,12 @@ const RESERVED_CONVEX_FILE_NAMES = new Set(["schema", "http", "crons"]);
56
56
 
57
57
  const jsIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
58
58
 
59
+ // Stricter than `jsIdentifierRegex`: tables cannot start with `_` (Convex
60
+ // reserves leading underscores for system tables) or `$` (Convex's table
61
+ // naming grammar does not accept it). Letters/digits/underscore only,
62
+ // letter-leading.
63
+ const tableNameRegex = /^[a-zA-Z][a-zA-Z0-9_]*$/;
64
+
59
65
  const isReservedJsIdentifier = (identifier: string) =>
60
66
  RESERVED_JS_IDENTIFIERS.has(identifier);
61
67
 
@@ -65,6 +71,9 @@ const isReservedConvexFileName = (fileName: string) =>
65
71
  const matchesJsIdentifierPattern = (identifier: string) =>
66
72
  jsIdentifierRegex.test(identifier);
67
73
 
74
+ const matchesTableNamePattern = (identifier: string) =>
75
+ tableNameRegex.test(identifier);
76
+
68
77
  export const validateConfectFunctionIdentifier = (identifier: string) => {
69
78
  if (!matchesJsIdentifierPattern(identifier)) {
70
79
  throw new Error(
@@ -84,3 +93,28 @@ export const validateConfectFunctionIdentifier = (identifier: string) => {
84
93
  );
85
94
  }
86
95
  };
96
+
97
+ /**
98
+ * Validate that `identifier` is suitable as a Convex table name (and, equivalently,
99
+ * as a `confect/tables/<identifier>.ts` filename).
100
+ *
101
+ * Rules:
102
+ * - Must match `/^[A-Za-z][A-Za-z0-9_]*$/` — letter-leading, alphanumeric plus
103
+ * underscore. No `$` (not a valid Convex table name character); no leading
104
+ * `_` (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.
107
+ */
108
+ export const validateConfectTableIdentifier = (identifier: string) => {
109
+ if (!matchesTableNamePattern(identifier)) {
110
+ throw new Error(
111
+ `Expected a valid Confect table identifier, but received: "${identifier}". Valid table identifiers must start with a letter and can only contain letters, numbers, and underscores. Leading underscores are reserved for Convex system tables.`,
112
+ );
113
+ }
114
+
115
+ if (isReservedJsIdentifier(identifier)) {
116
+ throw new Error(
117
+ `Expected a valid Confect table identifier, but received: "${identifier}". "${identifier}" is a reserved JavaScript identifier.`,
118
+ );
119
+ }
120
+ };
package/src/Lazy.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
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.
8
+ *
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.
15
+ *
16
+ * This is the single shared implementation consumed across packages (e.g.
17
+ * `@confect/core`'s lazy `FunctionSpec` schemas and `@confect/server`'s lazy
18
+ * `Table` `Fields` / `Doc` / `tableDefinition`), so there is no chance of the
19
+ * two drifting apart.
20
+ */
21
+ export const defineProperty = <T extends object, K extends PropertyKey>(
22
+ target: T,
23
+ key: K,
24
+ compute: () => unknown,
25
+ ): void => {
26
+ Object.defineProperty(target, key, {
27
+ configurable: true,
28
+ enumerable: true,
29
+ get(this: T) {
30
+ const value = compute();
31
+ Object.defineProperty(this, key, {
32
+ value,
33
+ writable: false,
34
+ enumerable: true,
35
+ configurable: false,
36
+ });
37
+ return value;
38
+ },
39
+ });
40
+ };
package/src/Ref.ts CHANGED
@@ -199,8 +199,7 @@ export const hasErrorSchema = (ref: Any): boolean =>
199
199
  Match.value(ref.functionSpec.functionProvenance).pipe(
200
200
  Match.tag(
201
201
  "Confect",
202
- (confectFunctionProvenance) =>
203
- confectFunctionProvenance.error !== undefined,
202
+ (confectFunctionProvenance) => "error" in confectFunctionProvenance,
204
203
  ),
205
204
  Match.tag("Convex", () => false),
206
205
  Match.exhaustive,
@@ -296,7 +295,7 @@ export const decodeError = <Ref_ extends Any>(
296
295
  ): Effect.Effect<Option.Option<Error<Ref_>>, ParseResult.ParseError> =>
297
296
  Match.value(ref.functionSpec.functionProvenance).pipe(
298
297
  Match.tag("Confect", (confectFunctionProvenance) =>
299
- confectFunctionProvenance.error !== undefined
298
+ "error" in confectFunctionProvenance
300
299
  ? Effect.map(
301
300
  Schema.decode(confectFunctionProvenance.error)(encodedError),
302
301
  Option.some,
@@ -317,7 +316,7 @@ export const decodeErrorSync = <Ref_ extends Any>(
317
316
  ): Option.Option<Error<Ref_>> =>
318
317
  Match.value(ref.functionSpec.functionProvenance).pipe(
319
318
  Match.tag("Confect", (confectFunctionProvenance) =>
320
- confectFunctionProvenance.error !== undefined
319
+ "error" in confectFunctionProvenance
321
320
  ? Option.some(
322
321
  Schema.decodeSync(confectFunctionProvenance.error)(
323
322
  encodedError,
@@ -336,7 +335,7 @@ export const maybeDecodeErrorSync = <Ref_ extends Any>(
336
335
  isConvexError(error)
337
336
  ? Match.value(ref.functionSpec.functionProvenance).pipe(
338
337
  Match.tag("Confect", (confectFunctionProvenance) =>
339
- confectFunctionProvenance.error !== undefined
338
+ "error" in confectFunctionProvenance
340
339
  ? Schema.decodeSync(confectFunctionProvenance.error)(error.data)
341
340
  : error,
342
341
  ),
package/src/Spec.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { Array, Option, Predicate, Record, String } from "effect";
1
+ import { Array, Option, Predicate, Record } from "effect";
2
2
  import * as GroupSpec from "./GroupSpec";
3
3
  import type * as RuntimeAndFunctionType from "./RuntimeAndFunctionType";
4
- import { validateConfectFunctionIdentifier } from "./internal/utils";
5
4
 
6
5
  export const TypeId = "@confect/core/Spec";
7
6
  export type TypeId = typeof TypeId;
@@ -33,13 +32,6 @@ export interface Spec<
33
32
  GroupName
34
33
  >;
35
34
  };
36
- /**
37
- * Mapping from an imported leaf `GroupSpec` reference to its full dot-path
38
- * within this spec tree. Populated by codegen-emitted `_generated/spec.ts`
39
- * via {@link Spec#addPath}; consumed by `FunctionImpl.make` /
40
- * `GroupImpl.make` to resolve a spec's location without walking the tree.
41
- */
42
- readonly paths: ReadonlyMap<GroupSpec.AnyWithProps, string>;
43
35
 
44
36
  add<Group extends GroupSpec.AnyWithPropsWithRuntime<Runtime>>(
45
37
  group: Group,
@@ -52,18 +44,6 @@ export interface Spec<
52
44
  name: Name,
53
45
  group: Group,
54
46
  ): Spec<Runtime, Groups_ | GroupSpec.NamedAt<Group, Name>>;
55
-
56
- /**
57
- * Register the imported leaf `group` at `path` within this spec's path
58
- * mapping. Returns a new `Spec` with one additional entry. The tree shape
59
- * (`groups`) is unaffected — registration and tree assembly are
60
- * independent steps, both performed by codegen in `_generated/spec.ts`.
61
- *
62
- * Re-registering the same group with the same path is a no-op (cheap
63
- * defense for codegen watch-mode re-imports). Re-registering with a
64
- * different path throws.
65
- */
66
- addPath(group: GroupSpec.AnyWithProps, path: string): Spec<Runtime, Groups_>;
67
47
  }
68
48
 
69
49
  export interface Any {
@@ -82,25 +62,6 @@ export interface AnyWithPropsWithRuntime<
82
62
  export type Groups<Spec_ extends AnyWithProps> =
83
63
  Spec_["groups"][keyof Spec_["groups"]];
84
64
 
85
- const validatePath = (path: string): void => {
86
- if (path.length === 0) {
87
- throw new Error(
88
- "Expected a non-empty Confect group path, but received an empty string.",
89
- );
90
- }
91
-
92
- const segments = String.split(path, ".");
93
-
94
- for (const segment of segments) {
95
- if (segment.length === 0) {
96
- throw new Error(
97
- `Expected a Confect group path made of dot-separated identifier segments, but received: "${path}".`,
98
- );
99
- }
100
- validateConfectFunctionIdentifier(segment);
101
- }
102
- };
103
-
104
65
  const Proto = {
105
66
  [TypeId]: TypeId,
106
67
 
@@ -108,7 +69,6 @@ const Proto = {
108
69
  return makeProto({
109
70
  runtime: this.runtime,
110
71
  groups: Record.set(this.groups, group.name, group),
111
- paths: this.paths,
112
72
  });
113
73
  },
114
74
 
@@ -120,30 +80,6 @@ const Proto = {
120
80
  return makeProto({
121
81
  runtime: this.runtime,
122
82
  groups: Record.set(this.groups, name, GroupSpec.withName(name, group)),
123
- paths: this.paths,
124
- });
125
- },
126
-
127
- addPath(this: AnyWithProps, group: GroupSpec.AnyWithProps, path: string) {
128
- validatePath(path);
129
-
130
- const existing = this.paths.get(group);
131
- if (existing !== undefined) {
132
- if (existing === path) {
133
- return this;
134
- }
135
- throw new Error(
136
- `Spec.addPath: the provided GroupSpec is already registered at "${existing}", but was re-registered at "${path}". Each GroupSpec must have at most one path.`,
137
- );
138
- }
139
-
140
- const nextPaths = new Map(this.paths);
141
- nextPaths.set(group, path);
142
-
143
- return makeProto({
144
- runtime: this.runtime,
145
- groups: this.groups,
146
- paths: nextPaths,
147
83
  });
148
84
  },
149
85
  };
@@ -154,32 +90,26 @@ const makeProto = <
154
90
  >({
155
91
  runtime,
156
92
  groups,
157
- paths,
158
93
  }: {
159
94
  runtime: Runtime;
160
95
  groups: Record.ReadonlyRecord<string, Groups_>;
161
- paths: ReadonlyMap<GroupSpec.AnyWithProps, string>;
162
96
  }): Spec<Runtime, Groups_> =>
163
97
  Object.assign(Object.create(Proto), {
164
98
  runtime,
165
99
  groups,
166
- paths,
167
100
  });
168
101
 
169
- const emptyPaths = (): ReadonlyMap<GroupSpec.AnyWithProps, string> => new Map();
170
-
171
102
  export const make = (): Spec<"Convex"> =>
172
- makeProto({ runtime: "Convex", groups: {}, paths: emptyPaths() });
103
+ makeProto({ runtime: "Convex", groups: {} });
173
104
 
174
105
  export const makeNode = (): Spec<"Node"> =>
175
- makeProto({ runtime: "Node", groups: {}, paths: emptyPaths() });
106
+ makeProto({ runtime: "Node", groups: {} });
176
107
 
177
108
  /**
178
- * Merges a Convex spec with an optional Node spec for use with `Api.make`.
109
+ * Merges a Convex spec with an optional Node spec into a single assembled
110
+ * spec (used by codegen to build `Refs.make` and to enumerate function paths).
179
111
  * When `nodeSpec` is provided, its groups are merged under a "node" namespace,
180
- * mirroring the structure used by `Refs.make`. The node spec's `paths`
181
- * entries are re-prefixed with `"node."` so they continue to identify the
182
- * same leaves at their new positions in the merged tree.
112
+ * mirroring the structure used by `Refs.make`.
183
113
  */
184
114
  export const merge = <
185
115
  ConvexSpec extends AnyWithPropsWithRuntime<"Convex">,
@@ -202,16 +132,8 @@ export const merge = <
202
132
  }),
203
133
  );
204
134
 
205
- const paths = new Map(convexSpec.paths);
206
- if (nodeSpec !== undefined) {
207
- for (const [group, path] of nodeSpec.paths) {
208
- paths.set(group, `node.${path}`);
209
- }
210
- }
211
-
212
135
  return Object.assign(Object.create(Proto), {
213
136
  runtime: "Convex" as const,
214
137
  groups,
215
- paths,
216
138
  }) as AnyWithProps;
217
139
  };
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ export * as FunctionSpec from "./FunctionSpec";
3
3
  export * as GenericId from "./GenericId";
4
4
  export * as GroupPath from "./GroupPath";
5
5
  export * as GroupSpec from "./GroupSpec";
6
+ export * as Identifier from "./Identifier";
7
+ export * as Lazy from "./Lazy";
6
8
  export * as PaginationResult from "./PaginationResult";
7
9
  export * as Ref from "./Ref";
8
10
  export * as Refs from "./Refs";
@@ -1,5 +0,0 @@
1
- //#region src/internal/utils.d.ts
2
- declare const validateConfectFunctionIdentifier: (identifier: string) => void;
3
- //#endregion
4
- export { validateConfectFunctionIdentifier };
5
- //# sourceMappingURL=utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","names":[],"sources":["../../src/internal/utils.ts"],"mappings":";cAmEa,iCAAA,GAAqC,UAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../src/internal/utils.ts"],"sourcesContent":["const RESERVED_JS_IDENTIFIERS = new Set([\n // Reserved keywords\n \"break\",\n \"case\",\n \"catch\",\n \"class\",\n \"const\",\n \"continue\",\n \"debugger\",\n \"default\",\n \"delete\",\n \"do\",\n \"else\",\n \"export\",\n \"extends\",\n \"finally\",\n \"for\",\n \"function\",\n \"if\",\n \"import\",\n \"in\",\n \"instanceof\",\n \"new\",\n \"return\",\n \"super\",\n \"switch\",\n \"this\",\n \"throw\",\n \"try\",\n \"typeof\",\n \"var\",\n \"void\",\n \"while\",\n \"with\",\n \"yield\",\n // Future reserved keywords\n \"await\",\n \"enum\",\n \"implements\",\n \"interface\",\n \"let\",\n \"package\",\n \"private\",\n \"protected\",\n \"public\",\n \"static\",\n // Literal values that cannot be reassigned\n \"null\",\n \"true\",\n \"false\",\n // Global objects that shouldn't be shadowed\n \"undefined\",\n]);\n\nconst RESERVED_CONVEX_FILE_NAMES = new Set([\"schema\", \"http\", \"crons\"]);\n\nconst jsIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;\n\nconst isReservedJsIdentifier = (identifier: string) =>\n RESERVED_JS_IDENTIFIERS.has(identifier);\n\nconst isReservedConvexFileName = (fileName: string) =>\n RESERVED_CONVEX_FILE_NAMES.has(fileName);\n\nconst matchesJsIdentifierPattern = (identifier: string) =>\n jsIdentifierRegex.test(identifier);\n\nexport const validateConfectFunctionIdentifier = (identifier: string) => {\n if (!matchesJsIdentifierPattern(identifier)) {\n throw new Error(\n `Expected a valid Confect function identifier, but received: \"${identifier}\". Valid identifiers must start with a letter, underscore, or dollar sign, and can only contain letters, numbers, underscores, or dollar signs.`,\n );\n }\n\n if (isReservedJsIdentifier(identifier)) {\n throw new Error(\n `Expected a valid Confect function identifier, but received: \"${identifier}\". \"${identifier}\" is a reserved JavaScript identifier.`,\n );\n }\n\n if (isReservedConvexFileName(identifier)) {\n throw new Error(\n `Expected a valid Confect function identifier, but received: \"${identifier}\". \"${identifier}\" is a reserved Convex file name.`,\n );\n }\n};\n"],"mappings":";AAAA,MAAM,0BAA0B,IAAI,IAAI;CAEtC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CAEA;CACD,CAAC;AAEF,MAAM,6BAA6B,IAAI,IAAI;CAAC;CAAU;CAAQ;CAAQ,CAAC;AAEvE,MAAM,oBAAoB;AAE1B,MAAM,0BAA0B,eAC9B,wBAAwB,IAAI,WAAW;AAEzC,MAAM,4BAA4B,aAChC,2BAA2B,IAAI,SAAS;AAE1C,MAAM,8BAA8B,eAClC,kBAAkB,KAAK,WAAW;AAEpC,MAAa,qCAAqC,eAAuB;AACvE,KAAI,CAAC,2BAA2B,WAAW,CACzC,OAAM,IAAI,MACR,gEAAgE,WAAW,iJAC5E;AAGH,KAAI,uBAAuB,WAAW,CACpC,OAAM,IAAI,MACR,gEAAgE,WAAW,MAAM,WAAW,wCAC7F;AAGH,KAAI,yBAAyB,WAAW,CACtC,OAAM,IAAI,MACR,gEAAgE,WAAW,MAAM,WAAW,mCAC7F"}