@confect/cli 9.0.0-next.5 → 9.0.0-next.7
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.
- package/CHANGELOG.md +165 -0
- package/dist/CodegenError.mjs +22 -9
- package/dist/CodegenError.mjs.map +1 -1
- package/dist/LeafModule.mjs +2 -2
- package/dist/LeafModule.mjs.map +1 -1
- package/dist/SpecAssemblyNode.mjs +1 -19
- package/dist/SpecAssemblyNode.mjs.map +1 -1
- package/dist/TableModule.mjs +90 -0
- package/dist/TableModule.mjs.map +1 -0
- package/dist/confect/codegen.mjs +175 -78
- package/dist/confect/codegen.mjs.map +1 -1
- package/dist/confect/dev.mjs +9 -7
- package/dist/confect/dev.mjs.map +1 -1
- package/dist/log.mjs +4 -3
- package/dist/log.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/dist/templates.mjs +123 -34
- package/dist/templates.mjs.map +1 -1
- package/package.json +41 -42
- package/src/CodegenError.ts +90 -28
- package/src/LeafModule.ts +1 -1
- package/src/SpecAssemblyNode.ts +0 -36
- package/src/TableModule.ts +153 -0
- package/src/confect/codegen.ts +323 -132
- package/src/confect/dev.ts +25 -7
- package/src/log.ts +4 -2
- package/src/templates.ts +200 -59
package/package.json
CHANGED
|
@@ -1,42 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@confect/cli",
|
|
3
|
-
"version": "9.0.0-next.5",
|
|
4
3
|
"description": "Developer tooling for codegen and sync",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"url": "https://github.com/rjdellecese/confect.git"
|
|
8
|
-
},
|
|
9
|
-
"bugs": {
|
|
10
|
-
"url": "https://github.com/rjdellecese/confect/issues"
|
|
11
|
-
},
|
|
12
|
-
"homepage": "https://confect.dev",
|
|
13
|
-
"sideEffects": false,
|
|
14
|
-
"type": "module",
|
|
4
|
+
"version": "9.0.0-next.7",
|
|
5
|
+
"author": "RJ Dellecese",
|
|
15
6
|
"bin": {
|
|
16
7
|
"confect": "./dist/index.mjs"
|
|
17
8
|
},
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"LICENSE",
|
|
21
|
-
"README.md",
|
|
22
|
-
"dist",
|
|
23
|
-
"package.json",
|
|
24
|
-
"src"
|
|
25
|
-
],
|
|
26
|
-
"exports": {
|
|
27
|
-
".": {
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"default": "./dist/index.js"
|
|
30
|
-
},
|
|
31
|
-
"./package.json": "./package.json"
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/rjdellecese/confect/issues"
|
|
32
11
|
},
|
|
33
|
-
"keywords": [
|
|
34
|
-
"effect",
|
|
35
|
-
"convex",
|
|
36
|
-
"cli"
|
|
37
|
-
],
|
|
38
|
-
"author": "RJ Dellecese",
|
|
39
|
-
"license": "ISC",
|
|
40
12
|
"dependencies": {
|
|
41
13
|
"@effect/cli": "^0.75.1",
|
|
42
14
|
"@effect/platform": "0.96.1",
|
|
@@ -64,26 +36,53 @@
|
|
|
64
36
|
"vite-tsconfig-paths": "6.1.1",
|
|
65
37
|
"vitest": "3.2.4"
|
|
66
38
|
},
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"effect": "^3.21.2",
|
|
69
|
-
"@confect/core": "^9.0.0-next.5",
|
|
70
|
-
"@confect/server": "^9.0.0-next.5"
|
|
71
|
-
},
|
|
72
39
|
"engines": {
|
|
73
|
-
"node": ">=22"
|
|
74
|
-
|
|
40
|
+
"node": ">=22"
|
|
41
|
+
},
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"default": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
75
48
|
},
|
|
49
|
+
"files": [
|
|
50
|
+
"CHANGELOG.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"README.md",
|
|
53
|
+
"dist",
|
|
54
|
+
"package.json",
|
|
55
|
+
"src"
|
|
56
|
+
],
|
|
57
|
+
"homepage": "https://confect.dev",
|
|
58
|
+
"keywords": [
|
|
59
|
+
"cli",
|
|
60
|
+
"convex",
|
|
61
|
+
"effect"
|
|
62
|
+
],
|
|
63
|
+
"license": "ISC",
|
|
76
64
|
"main": "./dist/index.js",
|
|
77
65
|
"module": "./dist/index.js",
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"effect": "^3.21.2",
|
|
68
|
+
"@confect/core": "^9.0.0-next.7",
|
|
69
|
+
"@confect/server": "^9.0.0-next.7"
|
|
70
|
+
},
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "https://github.com/rjdellecese/confect.git"
|
|
74
|
+
},
|
|
75
|
+
"sideEffects": false,
|
|
76
|
+
"type": "module",
|
|
78
77
|
"types": "./dist/index.d.ts",
|
|
79
78
|
"scripts": {
|
|
80
79
|
"build": "tsdown --config-loader unrun",
|
|
80
|
+
"clean": "rm -rf dist coverage node_modules",
|
|
81
81
|
"dev": "tsdown --watch --config-loader unrun",
|
|
82
|
-
"test": "vitest run",
|
|
83
|
-
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
84
82
|
"fix": "prettier --write . && eslint --fix . --max-warnings=0",
|
|
85
83
|
"format": "prettier --check .",
|
|
86
84
|
"lint": "eslint . --max-warnings=0",
|
|
87
|
-
"
|
|
85
|
+
"test": "vitest run",
|
|
86
|
+
"typecheck": "tsc --noEmit --project tsconfig.json"
|
|
88
87
|
}
|
|
89
88
|
}
|
package/src/CodegenError.ts
CHANGED
|
@@ -68,13 +68,6 @@ export class ImplMissingFunctionsError extends Schema.TaggedError<ImplMissingFun
|
|
|
68
68
|
},
|
|
69
69
|
) {}
|
|
70
70
|
|
|
71
|
-
export class SchemaInvalidDefaultExportError extends Schema.TaggedError<SchemaInvalidDefaultExportError>()(
|
|
72
|
-
"SchemaInvalidDefaultExportError",
|
|
73
|
-
{
|
|
74
|
-
schemaPath: Schema.String,
|
|
75
|
-
},
|
|
76
|
-
) {}
|
|
77
|
-
|
|
78
71
|
export class ParentChildNameCollisionError extends Schema.TaggedError<ParentChildNameCollisionError>()(
|
|
79
72
|
"ParentChildNameCollisionError",
|
|
80
73
|
{
|
|
@@ -85,8 +78,39 @@ export class ParentChildNameCollisionError extends Schema.TaggedError<ParentChil
|
|
|
85
78
|
},
|
|
86
79
|
) {}
|
|
87
80
|
|
|
88
|
-
export class
|
|
89
|
-
"
|
|
81
|
+
export class InvalidTableDefaultExportError extends Schema.TaggedError<InvalidTableDefaultExportError>()(
|
|
82
|
+
"InvalidTableDefaultExportError",
|
|
83
|
+
{
|
|
84
|
+
tablePath: Schema.String,
|
|
85
|
+
},
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
export class InvalidTableFilenameError extends Schema.TaggedError<InvalidTableFilenameError>()(
|
|
89
|
+
"InvalidTableFilenameError",
|
|
90
|
+
{
|
|
91
|
+
tablePath: Schema.String,
|
|
92
|
+
reason: Schema.String,
|
|
93
|
+
},
|
|
94
|
+
) {}
|
|
95
|
+
|
|
96
|
+
export class DuplicateTableNameError extends Schema.TaggedError<DuplicateTableNameError>()(
|
|
97
|
+
"DuplicateTableNameError",
|
|
98
|
+
{
|
|
99
|
+
// Every table name that more than one file resolves to, each paired with
|
|
100
|
+
// the colliding file paths. All collisions are captured in a single pass
|
|
101
|
+
// so the user can fix them together rather than re-running codegen once
|
|
102
|
+
// per conflict.
|
|
103
|
+
collisions: Schema.Array(
|
|
104
|
+
Schema.Struct({
|
|
105
|
+
tableName: Schema.String,
|
|
106
|
+
tablePaths: Schema.Array(Schema.String),
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
export class LegacySchemaFileError extends Schema.TaggedError<LegacySchemaFileError>()(
|
|
113
|
+
"LegacySchemaFileError",
|
|
90
114
|
{
|
|
91
115
|
schemaPath: Schema.String,
|
|
92
116
|
},
|
|
@@ -102,9 +126,11 @@ export const CodegenError = Schema.Union(
|
|
|
102
126
|
ImplMissingDefaultLayerError,
|
|
103
127
|
ImplNotFinalizedError,
|
|
104
128
|
ImplMissingFunctionsError,
|
|
105
|
-
SchemaInvalidDefaultExportError,
|
|
106
|
-
MissingSchemaFileError,
|
|
107
129
|
ParentChildNameCollisionError,
|
|
130
|
+
InvalidTableDefaultExportError,
|
|
131
|
+
InvalidTableFilenameError,
|
|
132
|
+
DuplicateTableNameError,
|
|
133
|
+
LegacySchemaFileError,
|
|
108
134
|
);
|
|
109
135
|
export type CodegenError = typeof CodegenError.Type;
|
|
110
136
|
|
|
@@ -207,7 +233,7 @@ const renderImplMissingDefaultLayerError = (
|
|
|
207
233
|
AnsiDoc.text("Impl "),
|
|
208
234
|
formatPathDoc(error.implPath),
|
|
209
235
|
AnsiDoc.text(
|
|
210
|
-
" must default-export a GroupImpl layer; wrap your handlers with `GroupImpl.make(
|
|
236
|
+
" must default-export a GroupImpl layer; wrap your handlers with `GroupImpl.make(databaseSchema, groupSpec).pipe(Layer.provide(...))` and `export default` it.",
|
|
211
237
|
),
|
|
212
238
|
);
|
|
213
239
|
|
|
@@ -218,7 +244,7 @@ const renderImplNotFinalizedError = (
|
|
|
218
244
|
AnsiDoc.text("Impl "),
|
|
219
245
|
formatPathDoc(error.implPath),
|
|
220
246
|
AnsiDoc.text(
|
|
221
|
-
" is not finalized; append `GroupImpl.finalize` to the end of the pipeline (e.g. `GroupImpl.make(
|
|
247
|
+
" is not finalized; append `GroupImpl.finalize` to the end of the pipeline (e.g. `GroupImpl.make(databaseSchema, group).pipe(Layer.provide(...), GroupImpl.finalize)`).",
|
|
222
248
|
),
|
|
223
249
|
);
|
|
224
250
|
|
|
@@ -235,25 +261,52 @@ const renderImplMissingFunctionsError = (
|
|
|
235
261
|
);
|
|
236
262
|
};
|
|
237
263
|
|
|
238
|
-
const
|
|
239
|
-
error:
|
|
264
|
+
const renderInvalidTableDefaultExportError = (
|
|
265
|
+
error: InvalidTableDefaultExportError,
|
|
240
266
|
): AnsiDoc.AnsiDoc =>
|
|
241
267
|
singleLine(
|
|
242
|
-
AnsiDoc.text("
|
|
243
|
-
formatPathDoc(error.
|
|
268
|
+
AnsiDoc.text("Table "),
|
|
269
|
+
formatPathDoc(error.tablePath),
|
|
244
270
|
AnsiDoc.text(
|
|
245
|
-
" must default-export a
|
|
271
|
+
" must default-export a Table (e.g. `export default Table.make({ ... })`); convert any named export to a default export.",
|
|
246
272
|
),
|
|
247
273
|
);
|
|
248
274
|
|
|
249
|
-
const
|
|
250
|
-
error:
|
|
275
|
+
const renderInvalidTableFilenameError = (
|
|
276
|
+
error: InvalidTableFilenameError,
|
|
251
277
|
): AnsiDoc.AnsiDoc =>
|
|
252
278
|
singleLine(
|
|
253
|
-
AnsiDoc.text("
|
|
279
|
+
AnsiDoc.text("Table "),
|
|
280
|
+
formatPathDoc(error.tablePath),
|
|
281
|
+
AnsiDoc.text(
|
|
282
|
+
` has an invalid filename: ${error.reason} Convex table names must start with a letter and contain only letters, numbers, and underscores; leading underscores are reserved for system tables.`,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const renderDuplicateTableNameError = (
|
|
287
|
+
error: DuplicateTableNameError,
|
|
288
|
+
): AnsiDoc.AnsiDoc => {
|
|
289
|
+
const conflicts = error.collisions
|
|
290
|
+
.map(
|
|
291
|
+
({ tableName, tablePaths }) =>
|
|
292
|
+
`\`${tableName}\` (${tablePaths.join(", ")})`,
|
|
293
|
+
)
|
|
294
|
+
.join("; ");
|
|
295
|
+
return singleLine(
|
|
296
|
+
AnsiDoc.text(
|
|
297
|
+
`Multiple files under \`confect/tables/\` resolve to the same table name. Table names are derived from filenames, so each must be unique across the directory (including subdirectories); rename or remove all but one. Conflicts: ${conflicts}.`,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const renderLegacySchemaFileError = (
|
|
303
|
+
error: LegacySchemaFileError,
|
|
304
|
+
): AnsiDoc.AnsiDoc =>
|
|
305
|
+
singleLine(
|
|
306
|
+
AnsiDoc.text("Found a legacy "),
|
|
254
307
|
formatPathDoc(error.schemaPath),
|
|
255
308
|
AnsiDoc.text(
|
|
256
|
-
"
|
|
309
|
+
". Delete it: tables in `confect/tables/*.ts` are now the single source of truth, and the runtime schema is generated as `confect/_generated/schema.ts`.",
|
|
257
310
|
),
|
|
258
311
|
);
|
|
259
312
|
|
|
@@ -320,24 +373,33 @@ export const renderCodegenError = (error: CodegenError): string => {
|
|
|
320
373
|
AnsiDoc.render({ style: "pretty" }),
|
|
321
374
|
),
|
|
322
375
|
),
|
|
323
|
-
Match.tag("
|
|
376
|
+
Match.tag("ParentChildNameCollisionError", (e) =>
|
|
324
377
|
pipe(
|
|
325
|
-
|
|
378
|
+
renderParentChildNameCollisionError(e),
|
|
326
379
|
AnsiDoc.render({ style: "pretty" }),
|
|
327
380
|
),
|
|
328
381
|
),
|
|
329
|
-
Match.tag("
|
|
382
|
+
Match.tag("InvalidTableDefaultExportError", (e) =>
|
|
330
383
|
pipe(
|
|
331
|
-
|
|
384
|
+
renderInvalidTableDefaultExportError(e),
|
|
332
385
|
AnsiDoc.render({ style: "pretty" }),
|
|
333
386
|
),
|
|
334
387
|
),
|
|
335
|
-
Match.tag("
|
|
388
|
+
Match.tag("InvalidTableFilenameError", (e) =>
|
|
336
389
|
pipe(
|
|
337
|
-
|
|
390
|
+
renderInvalidTableFilenameError(e),
|
|
338
391
|
AnsiDoc.render({ style: "pretty" }),
|
|
339
392
|
),
|
|
340
393
|
),
|
|
394
|
+
Match.tag("DuplicateTableNameError", (e) =>
|
|
395
|
+
pipe(
|
|
396
|
+
renderDuplicateTableNameError(e),
|
|
397
|
+
AnsiDoc.render({ style: "pretty" }),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
Match.tag("LegacySchemaFileError", (e) =>
|
|
401
|
+
pipe(renderLegacySchemaFileError(e), AnsiDoc.render({ style: "pretty" })),
|
|
402
|
+
),
|
|
341
403
|
Match.exhaustive,
|
|
342
404
|
);
|
|
343
405
|
};
|
package/src/LeafModule.ts
CHANGED
|
@@ -310,7 +310,7 @@ export const validateImpl = (leaf: LeafModule) =>
|
|
|
310
310
|
if (missing.length > 0) {
|
|
311
311
|
return yield* new ImplMissingFunctionsError({
|
|
312
312
|
implPath: implRelativePath,
|
|
313
|
-
groupPath:
|
|
313
|
+
groupPath: leaf.groupPathDot,
|
|
314
314
|
missingFunctionNames: missing,
|
|
315
315
|
});
|
|
316
316
|
}
|
package/src/SpecAssemblyNode.ts
CHANGED
|
@@ -82,39 +82,3 @@ export const collectImportBindings = (
|
|
|
82
82
|
Array.map(([, binding]) => binding),
|
|
83
83
|
Array.sortBy(Order.mapInput(Order.string, (binding) => binding.localName)),
|
|
84
84
|
);
|
|
85
|
-
|
|
86
|
-
export interface SpecLeafPath {
|
|
87
|
-
readonly binding: SpecImportBinding;
|
|
88
|
-
readonly dotPath: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const leafPathsForNode = (
|
|
92
|
-
node: SpecAssemblyNode,
|
|
93
|
-
ancestorSegments: ReadonlyArray<string>,
|
|
94
|
-
): ReadonlyArray<SpecLeafPath> => {
|
|
95
|
-
const segments = [...ancestorSegments, node.segment];
|
|
96
|
-
const childPaths = Array.flatMap(node.children, (child) =>
|
|
97
|
-
leafPathsForNode(child, segments),
|
|
98
|
-
);
|
|
99
|
-
return Option.match(node.importBinding, {
|
|
100
|
-
onNone: () => childPaths,
|
|
101
|
-
onSome: (binding) =>
|
|
102
|
-
Array.prepend(childPaths, { binding, dotPath: segments.join(".") }),
|
|
103
|
-
});
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Walk the assembly tree and produce one entry per leaf spec, pairing its
|
|
108
|
-
* import binding with the full dot-path codegen will register via
|
|
109
|
-
* `Spec.addPath`. Ordering matches `collectImportBindings` (sorted by the
|
|
110
|
-
* binding's local name) so the generated file is stable across runs.
|
|
111
|
-
*/
|
|
112
|
-
export const collectLeafPaths = (
|
|
113
|
-
nodes: ReadonlyArray<SpecAssemblyNode>,
|
|
114
|
-
): ReadonlyArray<SpecLeafPath> =>
|
|
115
|
-
pipe(
|
|
116
|
-
Array.flatMap(nodes, (node) => leafPathsForNode(node, [])),
|
|
117
|
-
Array.sortBy(
|
|
118
|
-
Order.mapInput(Order.string, (entry) => entry.binding.localName),
|
|
119
|
-
),
|
|
120
|
-
);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Identifier } from "@confect/core";
|
|
2
|
+
import * as Table from "@confect/server/Table";
|
|
3
|
+
import { FileSystem, Path } from "@effect/platform";
|
|
4
|
+
import { Array, Effect, Order, pipe } from "effect";
|
|
5
|
+
import { fromBundlerError } from "./BuildError";
|
|
6
|
+
import * as Bundler from "./Bundler";
|
|
7
|
+
import {
|
|
8
|
+
DuplicateTableNameError,
|
|
9
|
+
InvalidTableDefaultExportError,
|
|
10
|
+
InvalidTableFilenameError,
|
|
11
|
+
} from "./CodegenError";
|
|
12
|
+
import { ConfectDirectory } from "./ConfectDirectory";
|
|
13
|
+
|
|
14
|
+
export const TABLES_DIRNAME = "tables";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Discovered metadata for a single user-authored table module under
|
|
18
|
+
* `confect/tables/`.
|
|
19
|
+
*
|
|
20
|
+
* - `relativePath` — path from `confect/` to the file (e.g. `tables/notes.ts`).
|
|
21
|
+
* - `tableName` — the file basename (e.g. `notes`). This is also the import
|
|
22
|
+
* binding used in generated files, and the table name surfaced to Convex.
|
|
23
|
+
*/
|
|
24
|
+
export interface TableModule {
|
|
25
|
+
readonly relativePath: string;
|
|
26
|
+
readonly tableName: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tableNameFromRelativePath = (relativePath: string) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const path = yield* Path.Path;
|
|
32
|
+
const { name } = path.parse(relativePath);
|
|
33
|
+
return name;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const listTableFiles = Effect.gen(function* () {
|
|
37
|
+
const fs = yield* FileSystem.FileSystem;
|
|
38
|
+
const path = yield* Path.Path;
|
|
39
|
+
const confectDirectory = yield* ConfectDirectory.get;
|
|
40
|
+
const tablesDirectory = path.join(confectDirectory, TABLES_DIRNAME);
|
|
41
|
+
|
|
42
|
+
if (!(yield* fs.exists(tablesDirectory))) {
|
|
43
|
+
return [] as ReadonlyArray<string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const allPaths = yield* fs.readDirectory(tablesDirectory, {
|
|
47
|
+
recursive: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return pipe(
|
|
51
|
+
allPaths,
|
|
52
|
+
Array.filter((p) => p.endsWith(".ts") && !p.endsWith(".test.ts")),
|
|
53
|
+
Array.map((p) => path.join(TABLES_DIRNAME, p)),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const byTableName = Order.mapInput(
|
|
58
|
+
Order.string,
|
|
59
|
+
(tableModule: TableModule) => tableModule.tableName,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Discover every `confect/tables/**\/*.ts` module by listing the directory.
|
|
64
|
+
* Validates that each filename is a legal table identifier — the table name is
|
|
65
|
+
* derived from the file basename, so the filename must be a valid JavaScript
|
|
66
|
+
* identifier with no leading underscore (Convex reserves `_<name>` for system
|
|
67
|
+
* tables).
|
|
68
|
+
*
|
|
69
|
+
* This step does *not* bundle the table modules. It runs early in the codegen
|
|
70
|
+
* pipeline so the `_generated/id.ts` constructor can be emitted *before* any
|
|
71
|
+
* user-authored table is bundled (those modules import from `_generated/id.ts`
|
|
72
|
+
* for cross-table refs).
|
|
73
|
+
*
|
|
74
|
+
* A missing `confect/tables/` directory is allowed and produces an empty list.
|
|
75
|
+
*
|
|
76
|
+
* Fails with {@link InvalidTableFilenameError} if any filename is not a valid
|
|
77
|
+
* table identifier, or {@link DuplicateTableNameError} if two files resolve to
|
|
78
|
+
* the same table name (the directory is scanned recursively but names are
|
|
79
|
+
* derived from the basename alone, so `tables/a/notes.ts` and
|
|
80
|
+
* `tables/b/notes.ts` would collide).
|
|
81
|
+
*/
|
|
82
|
+
export const discover = Effect.gen(function* () {
|
|
83
|
+
const relativePaths = yield* listTableFiles;
|
|
84
|
+
|
|
85
|
+
const tableModules = yield* Effect.forEach(
|
|
86
|
+
relativePaths,
|
|
87
|
+
(relativePath) =>
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
const tableName = yield* tableNameFromRelativePath(relativePath);
|
|
90
|
+
yield* Effect.try({
|
|
91
|
+
try: () => Identifier.validateConfectTableIdentifier(tableName),
|
|
92
|
+
catch: (e) =>
|
|
93
|
+
new InvalidTableFilenameError({
|
|
94
|
+
tablePath: relativePath,
|
|
95
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
return { relativePath, tableName } satisfies TableModule;
|
|
99
|
+
}),
|
|
100
|
+
{ concurrency: "unbounded" },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const sorted = pipe(tableModules, Array.sortBy(byTableName));
|
|
104
|
+
|
|
105
|
+
const collisions = Object.entries(
|
|
106
|
+
Array.groupBy(sorted, (tableModule) => tableModule.tableName),
|
|
107
|
+
)
|
|
108
|
+
.filter(([, group]) => group.length > 1)
|
|
109
|
+
.map(([tableName, group]) => ({
|
|
110
|
+
tableName,
|
|
111
|
+
tablePaths: Array.map(group, (tableModule) => tableModule.relativePath),
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
if (collisions.length > 0) {
|
|
115
|
+
return yield* new DuplicateTableNameError({ collisions });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return sorted;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Bundle every discovered table module and verify that its default export is
|
|
123
|
+
* an {@link Table.UnnamedTable} (the result of `Table.make(...)` before a
|
|
124
|
+
* name has been bound). Fails with {@link InvalidTableDefaultExportError} if
|
|
125
|
+
* any module's default export is missing or has the wrong shape.
|
|
126
|
+
*
|
|
127
|
+
* Must run *after* `_generated/id.ts` has been emitted, because user-authored
|
|
128
|
+
* table modules typically `import { Id } from "../_generated/id"` for
|
|
129
|
+
* cross-table references.
|
|
130
|
+
*/
|
|
131
|
+
export const validate = (tableModules: ReadonlyArray<TableModule>) =>
|
|
132
|
+
Effect.gen(function* () {
|
|
133
|
+
const path = yield* Path.Path;
|
|
134
|
+
const confectDirectory = yield* ConfectDirectory.get;
|
|
135
|
+
|
|
136
|
+
yield* Effect.forEach(
|
|
137
|
+
tableModules,
|
|
138
|
+
({ relativePath }) =>
|
|
139
|
+
Effect.gen(function* () {
|
|
140
|
+
const absolutePath = path.resolve(confectDirectory, relativePath);
|
|
141
|
+
const { module } = yield* Bundler.bundle(absolutePath).pipe(
|
|
142
|
+
Effect.mapError((error) => fromBundlerError(relativePath, error)),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
if (!Table.isUnnamedTable(module.default)) {
|
|
146
|
+
return yield* new InvalidTableDefaultExportError({
|
|
147
|
+
tablePath: relativePath,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
{ concurrency: "unbounded" },
|
|
152
|
+
);
|
|
153
|
+
});
|