@confect/cli 9.0.0-next.9 → 9.0.1

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.
@@ -1,847 +0,0 @@
1
- import { Spec, type GroupSpec } from "@confect/core";
2
- import * as Command from "@effect/cli/Command";
3
- import * as FileSystem from "@effect/platform/FileSystem";
4
- import * as Path from "@effect/platform/Path";
5
- import * as Array from "effect/Array";
6
- import * as Effect from "effect/Effect";
7
- import * as Either from "effect/Either";
8
- import * as HashSet from "effect/HashSet";
9
- import * as Match from "effect/Match";
10
- import * as Option from "effect/Option";
11
- import * as Ref from "effect/Ref";
12
- import * as Bundler from "../Bundler";
13
- import * as CodegenError from "../CodegenError";
14
- import {
15
- LegacySchemaFileError,
16
- MissingImplFileError,
17
- MissingSpecFileError,
18
- ParentChildNameCollisionError,
19
- } from "../CodegenError";
20
- import { ConfectDirectory } from "../ConfectDirectory";
21
- import { ConvexDirectory } from "../ConvexDirectory";
22
- import * as FunctionPaths from "../FunctionPaths";
23
- import {
24
- discoverLeafImplFiles,
25
- discoverLeafSpecFiles,
26
- implPathForSpec,
27
- registeredFunctionsRelativePath,
28
- specPathForImpl,
29
- toLeafModule,
30
- validateImpl,
31
- validateSpec,
32
- type LeafModule,
33
- } from "../LeafModule";
34
- import {
35
- logFileAdded,
36
- logFileModified,
37
- logFileRemoved,
38
- logPending,
39
- logSuccess,
40
- logWarn,
41
- } from "../log";
42
- import {
43
- assemblyNodesFromLeaves,
44
- type SpecAssemblyNode,
45
- } from "../SpecAssemblyNode";
46
- import * as TableModule from "../TableModule";
47
- import * as templates from "../templates";
48
- import {
49
- generateAuthConfig,
50
- generateCrons,
51
- generateFunctions,
52
- generateHttp,
53
- removePathIfExists,
54
- toModuleImportPath,
55
- touchConvexSchema,
56
- writeFileStringAndLog,
57
- WriteTracker,
58
- } from "../utils";
59
-
60
- const GENERATED_DIRNAME = "_generated";
61
-
62
- const GENERATED_SPEC_PATH = Effect.andThen(Path.Path, (path) =>
63
- path.join(GENERATED_DIRNAME, "spec.ts"),
64
- );
65
- const GENERATED_SCHEMA_PATH = Effect.andThen(Path.Path, (path) =>
66
- path.join(GENERATED_DIRNAME, "schema.ts"),
67
- );
68
- const GENERATED_CONVEX_SCHEMA_PATH = Effect.andThen(Path.Path, (path) =>
69
- path.join(GENERATED_DIRNAME, "convexSchema.ts"),
70
- );
71
- const GENERATED_ID_PATH = Effect.andThen(Path.Path, (path) =>
72
- path.join(GENERATED_DIRNAME, "id.ts"),
73
- );
74
- const GENERATED_TABLES_DIRNAME = Effect.andThen(Path.Path, (path) =>
75
- path.join(GENERATED_DIRNAME, "tables"),
76
- );
77
-
78
- const LEGACY_PATHS = Effect.gen(function* () {
79
- const path = yield* Path.Path;
80
-
81
- return [
82
- "spec.ts",
83
- "nodeSpec.ts",
84
- "impl.ts",
85
- "nodeImpl.ts",
86
- path.join(GENERATED_DIRNAME, "registeredFunctions.ts"),
87
- path.join(GENERATED_DIRNAME, "nodeRegisteredFunctions.ts"),
88
- path.join(GENERATED_DIRNAME, "impl.ts"),
89
- path.join(GENERATED_DIRNAME, "nodeImpl.ts"),
90
- // `_generated/nodeSpec.ts` is not part of the generated output (all groups
91
- // live in `_generated/spec.ts`); delete any copy left by an older version.
92
- path.join(GENERATED_DIRNAME, "nodeSpec.ts"),
93
- ];
94
- });
95
-
96
- export const codegen = Command.make("codegen", {}, () =>
97
- Effect.gen(function* () {
98
- yield* logPending("Performing initial sync…");
99
- yield* codegenHandler.pipe(
100
- Effect.asVoid,
101
- Effect.tap(() => logSuccess("Generated files are up-to-date")),
102
- CodegenError.tapAndLog,
103
- );
104
- }),
105
- ).pipe(
106
- Command.withDescription(
107
- "Generate `confect/_generated` files and the contents of the `convex` directory (except `convex.config.ts` and `tsconfig.json`)",
108
- ),
109
- );
110
-
111
- export const codegenHandler = Effect.gen(function* () {
112
- const tracker = yield* Ref.make(false);
113
-
114
- const functionPaths = yield* runCodegen.pipe(
115
- Effect.provideService(WriteTracker, tracker),
116
- );
117
-
118
- const anyWritesHappened = yield* Ref.get(tracker);
119
- return { functionPaths, anyWritesHappened };
120
- });
121
-
122
- const runCodegen = Effect.gen(function* () {
123
- yield* generateConfectGeneratedDirectory;
124
- // Reject a legacy `confect/schema.ts` up front so the user-facing
125
- // migration message surfaces before any bundler error from impl
126
- // validation (each impl imports `_generated/schema.ts`).
127
- yield* rejectLegacySchemaFile;
128
- // List `confect/tables/*.ts` (filename-only — no bundling yet) so the
129
- // `_generated/id.ts` constructor can be emitted *before* we bundle any
130
- // user-authored table module. Tables import from `_generated/id.ts` for
131
- // cross-table id refs, so it must exist on disk first.
132
- const tableModules = yield* TableModule.discover;
133
- yield* warnIfNoTables(tableModules);
134
- yield* generateIdConstructor(tableModules);
135
- // Now that `_generated/id.ts` is on disk, bundle each table module and
136
- // check its default export is an `UnnamedTable`. Surface diagnostics
137
- // here (rather than later) so they appear before impl-validation noise.
138
- yield* TableModule.validate(tableModules);
139
- yield* generateTableWrappers(tableModules);
140
- yield* removeObsoleteTableWrappers(tableModules);
141
- yield* generateRuntimeSchema(tableModules);
142
- const { leaves, groupSpecsByRelativePath } =
143
- yield* loadAndValidateLeafModules;
144
- yield* removeLegacyFiles;
145
- yield* validateNoParentChildNameCollisions(leaves, groupSpecsByRelativePath);
146
- yield* generateAssembledSpecs(leaves);
147
- // `_generated/api.ts` / `nodeApi.ts` are no longer imported by generated or
148
- // impl code (impls take the database schema from `_generated/schema`
149
- // directly), so remove any copies left over from earlier versions before
150
- // impl validation runs.
151
- yield* Effect.all(
152
- [
153
- removeGeneratedApi,
154
- generateRefs,
155
- removeGeneratedNodeApi,
156
- generateServices,
157
- generateConvexSchema(tableModules),
158
- ],
159
- { concurrency: "unbounded" },
160
- );
161
- yield* validateImplModules(leaves);
162
- yield* generateGroupRegisteredFunctions(leaves);
163
- yield* removeObsoleteRegisteredFunctions(leaves);
164
- const [functionPaths] = yield* Effect.all(
165
- [
166
- generateFunctionModules,
167
- generateConvexSchemaReexport,
168
- logGenerated(generateHttp),
169
- logGenerated(generateCrons),
170
- logGenerated(generateAuthConfig),
171
- ],
172
- { concurrency: "unbounded" },
173
- );
174
- yield* touchConvexSchema;
175
- return functionPaths;
176
- });
177
-
178
- const generateConfectGeneratedDirectory = Effect.gen(function* () {
179
- const fs = yield* FileSystem.FileSystem;
180
- const path = yield* Path.Path;
181
- const confectDirectory = yield* ConfectDirectory.get;
182
-
183
- if (!(yield* fs.exists(path.join(confectDirectory, "_generated")))) {
184
- yield* fs.makeDirectory(path.join(confectDirectory, "_generated"), {
185
- recursive: true,
186
- });
187
- yield* logFileAdded(path.join(confectDirectory, "_generated") + "/");
188
- }
189
- });
190
-
191
- const loadAndValidateLeafModules = Effect.gen(function* () {
192
- const fs = yield* FileSystem.FileSystem;
193
- const path = yield* Path.Path;
194
- const confectDirectory = yield* ConfectDirectory.get;
195
- const specFiles = yield* discoverLeafSpecFiles;
196
-
197
- const results = yield* Effect.forEach(specFiles, (specRelativePath) =>
198
- Effect.gen(function* () {
199
- const discovered = yield* toLeafModule(specRelativePath);
200
- const groupSpec = yield* validateSpec(discovered);
201
- // Fill in the runtime now that the spec is bundled; discovery left it `None`.
202
- const leaf = {
203
- ...discovered,
204
- runtime: Option.some(groupSpec.runtime),
205
- };
206
-
207
- const implRelativePath = yield* implPathForSpec(specRelativePath);
208
- const implAbsolutePath = path.join(confectDirectory, implRelativePath);
209
- if (!(yield* fs.exists(implAbsolutePath))) {
210
- return yield* new MissingImplFileError({
211
- specPath: specRelativePath,
212
- expectedImplPath: implRelativePath,
213
- });
214
- }
215
-
216
- return { leaf, groupSpec };
217
- }),
218
- );
219
-
220
- yield* validateOrphanImpls(specFiles);
221
-
222
- const leaves = Array.map(results, ({ leaf }) => leaf);
223
- const groupSpecsByRelativePath = new Map(
224
- Array.map(results, ({ leaf, groupSpec }) => [leaf.relativePath, groupSpec]),
225
- );
226
-
227
- return { leaves, groupSpecsByRelativePath };
228
- });
229
-
230
- /**
231
- * Walk the assembly tree and fail with a {@link ParentChildNameCollisionError}
232
- * when a parent leaf declares a function or subgroup whose name matches a
233
- * sibling subdirectory spec's segment. Without this check the colliding
234
- * descendant would overwrite the parent's entry in the assembled
235
- * `GroupSpec.groups` map at runtime, surfacing as a confusing
236
- * `Refs.make` error rather than a codegen-time diagnostic.
237
- */
238
- export const validateNoParentChildNameCollisions = (
239
- leaves: ReadonlyArray<LeafModule>,
240
- groupSpecsByRelativePath: ReadonlyMap<string, GroupSpec.AnyWithProps>,
241
- ) =>
242
- Effect.gen(function* () {
243
- // Convex and Node groups share one namespace, so they assemble into a
244
- // single tree. A Node group nested under a Convex parent (or vice versa) is
245
- // caught here by the parent/child collision check.
246
- const nodes = assemblyNodesFromLeaves(leaves);
247
- yield* Effect.forEach(nodes, (n) =>
248
- checkAssemblyNodeForCollisions(n, groupSpecsByRelativePath),
249
- );
250
- });
251
-
252
- const checkAssemblyNodeForCollisions = (
253
- node: SpecAssemblyNode,
254
- groupSpecsByRelativePath: ReadonlyMap<string, GroupSpec.AnyWithProps>,
255
- ): Effect.Effect<void, ParentChildNameCollisionError> =>
256
- Effect.gen(function* () {
257
- yield* Option.match(node.importBinding, {
258
- onNone: () => Effect.void,
259
- onSome: (binding) =>
260
- Effect.gen(function* () {
261
- if (node.children.length === 0) return;
262
- const parentRelativePath = bindingToRelativeSpecPath(
263
- binding.importPath,
264
- );
265
- const parentGroupSpec =
266
- groupSpecsByRelativePath.get(parentRelativePath);
267
- if (parentGroupSpec === undefined) return;
268
- yield* Effect.forEach(node.children, (child) => {
269
- if (
270
- Object.prototype.hasOwnProperty.call(
271
- parentGroupSpec.functions,
272
- child.segment,
273
- )
274
- ) {
275
- return Effect.fail(
276
- new ParentChildNameCollisionError({
277
- parentSpecPath: parentRelativePath,
278
- childSpecPath: childRepresentativeSpecPath(child),
279
- collisionName: child.segment,
280
- collisionKind: "function",
281
- }),
282
- );
283
- }
284
- if (
285
- Object.prototype.hasOwnProperty.call(
286
- parentGroupSpec.groups,
287
- child.segment,
288
- )
289
- ) {
290
- return Effect.fail(
291
- new ParentChildNameCollisionError({
292
- parentSpecPath: parentRelativePath,
293
- childSpecPath: childRepresentativeSpecPath(child),
294
- collisionName: child.segment,
295
- collisionKind: "group",
296
- }),
297
- );
298
- }
299
- return Effect.void;
300
- });
301
- }),
302
- });
303
- yield* Effect.forEach(node.children, (child) =>
304
- checkAssemblyNodeForCollisions(child, groupSpecsByRelativePath),
305
- );
306
- });
307
-
308
- /**
309
- * `LeafModule.specImportPath` is the import path used from inside the
310
- * generated `_generated/spec.ts` (e.g. `"../notes.spec"`). Strip the
311
- * `../` prefix and re-add the `.ts` extension to recover the leaf's
312
- * confect-relative spec path used as the key in
313
- * `groupSpecsByRelativePath`.
314
- */
315
- const bindingToRelativeSpecPath = (importPath: string): string => {
316
- const withoutDotDot = importPath.startsWith("../")
317
- ? importPath.slice(3)
318
- : importPath;
319
- return `${withoutDotDot}.ts`;
320
- };
321
-
322
- /**
323
- * A child assembly node may itself be a parent without a leaf (when the
324
- * actual leaves live only in deeper subdirectories). In that case we
325
- * surface the first descendant leaf as a representative path so the
326
- * error message points at something the user actually wrote.
327
- */
328
- const childRepresentativeSpecPath = (node: SpecAssemblyNode): string => {
329
- if (Option.isSome(node.importBinding)) {
330
- return bindingToRelativeSpecPath(node.importBinding.value.importPath);
331
- }
332
- for (const child of node.children) {
333
- return childRepresentativeSpecPath(child);
334
- }
335
- return node.segment;
336
- };
337
-
338
- const validateOrphanImpls = (specFiles: ReadonlyArray<string>) =>
339
- Effect.gen(function* () {
340
- const fs = yield* FileSystem.FileSystem;
341
- const path = yield* Path.Path;
342
- const confectDirectory = yield* ConfectDirectory.get;
343
- const implFiles = yield* discoverLeafImplFiles;
344
- const specPaths = new Set(specFiles);
345
-
346
- yield* Effect.forEach(implFiles, (implRelativePath) =>
347
- Effect.gen(function* () {
348
- const specRelativePath = yield* specPathForImpl(implRelativePath);
349
- if (specPaths.has(specRelativePath)) {
350
- return;
351
- }
352
-
353
- const specAbsolutePath = path.join(confectDirectory, specRelativePath);
354
- if (!(yield* fs.exists(specAbsolutePath))) {
355
- return yield* new MissingSpecFileError({
356
- implPath: implRelativePath,
357
- expectedSpecPath: specRelativePath,
358
- });
359
- }
360
- }),
361
- );
362
- });
363
-
364
- const removeLegacyFiles = Effect.gen(function* () {
365
- const fs = yield* FileSystem.FileSystem;
366
- const path = yield* Path.Path;
367
- const confectDirectory = yield* ConfectDirectory.get;
368
- const legacyPaths = yield* LEGACY_PATHS;
369
-
370
- yield* Effect.forEach(legacyPaths, (relativePath) =>
371
- Effect.gen(function* () {
372
- const absolutePath = path.join(confectDirectory, relativePath);
373
- if (yield* fs.exists(absolutePath)) {
374
- yield* removePathIfExists(absolutePath);
375
- yield* logFileRemoved(absolutePath);
376
- }
377
- }),
378
- );
379
- });
380
-
381
- const generateAssembledSpecs = (leaves: ReadonlyArray<LeafModule>) =>
382
- Effect.gen(function* () {
383
- const path = yield* Path.Path;
384
- const confectDirectory = yield* ConfectDirectory.get;
385
- const generatedSpecPath = yield* GENERATED_SPEC_PATH;
386
-
387
- // A single assembled spec holds every group regardless of runtime — a Node
388
- // group's `makeNode()` lives in its imported leaf spec, so the assembled
389
- // file is runtime-agnostic. Always emit it (even empty) so downstream
390
- // readers (`loadGeneratedSpec`, `generateRefs`) always find a spec module.
391
- const nodes = assemblyNodesFromLeaves(leaves);
392
- const specContents = yield* templates.assembledSpec({ nodes });
393
- yield* writeFileStringAndLog(
394
- path.join(confectDirectory, generatedSpecPath),
395
- specContents,
396
- );
397
- });
398
-
399
- const validateImplModules = (leaves: ReadonlyArray<LeafModule>) =>
400
- Effect.forEach(leaves, validateImpl);
401
-
402
- const generateGroupRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>
403
- Effect.gen(function* () {
404
- const path = yield* Path.Path;
405
- const confectDirectory = yield* ConfectDirectory.get;
406
-
407
- yield* Effect.forEach(leaves, (leaf) =>
408
- Effect.gen(function* () {
409
- const registryRelativePath =
410
- yield* registeredFunctionsRelativePath(leaf);
411
- const registryPath = path.join(
412
- confectDirectory,
413
- "_generated",
414
- registryRelativePath,
415
- );
416
- const registryDir = path.dirname(registryPath);
417
- const fs = yield* FileSystem.FileSystem;
418
- if (!(yield* fs.exists(registryDir))) {
419
- yield* fs.makeDirectory(registryDir, { recursive: true });
420
- }
421
-
422
- const implRelativePath = yield* implPathForSpec(leaf.relativePath);
423
- const schemaImportPath = yield* toModuleImportPath(
424
- path.relative(
425
- path.dirname(registryPath),
426
- path.join(confectDirectory, "_generated", "schema.ts"),
427
- ),
428
- );
429
- // The group's own leaf spec (sibling of its impl), referenced
430
- // type-only by the registry to shape its returned record.
431
- const specImportPath = yield* toModuleImportPath(
432
- path.relative(
433
- path.dirname(registryPath),
434
- path.join(confectDirectory, leaf.relativePath),
435
- ),
436
- );
437
- const implImportPath = yield* toModuleImportPath(
438
- path.relative(
439
- path.dirname(registryPath),
440
- path.join(confectDirectory, implRelativePath),
441
- ),
442
- );
443
-
444
- // Every leaf reaching this point came through
445
- // `loadAndValidateLeafModules`, which stamps the runtime from the
446
- // validated spec — so `None` here means that invariant was broken.
447
- const runtime = yield* Option.match(leaf.runtime, {
448
- onNone: () =>
449
- Effect.dieMessage(
450
- `Runtime for '${leaf.relativePath}' was not resolved before registry generation.`,
451
- ),
452
- onSome: Effect.succeed,
453
- });
454
-
455
- const contents = yield* templates.registeredFunctionsForGroup({
456
- schemaImportPath,
457
- specImportPath,
458
- implImportPath,
459
- layerExportName: leaf.exportName,
460
- useNode: runtime === "Node",
461
- });
462
-
463
- yield* writeFileStringAndLog(registryPath, contents);
464
- }),
465
- );
466
- });
467
-
468
- const removeObsoleteRegisteredFunctions = (leaves: ReadonlyArray<LeafModule>) =>
469
- Effect.gen(function* () {
470
- const fs = yield* FileSystem.FileSystem;
471
- const path = yield* Path.Path;
472
- const confectDirectory = yield* ConfectDirectory.get;
473
- const registryRoot = path.join(
474
- confectDirectory,
475
- "_generated",
476
- "registeredFunctions",
477
- );
478
-
479
- if (!(yield* fs.exists(registryRoot))) {
480
- return;
481
- }
482
-
483
- const expected = new Set(
484
- yield* Effect.forEach(leaves, (leaf) =>
485
- registeredFunctionsRelativePath(leaf),
486
- ),
487
- );
488
-
489
- const existing = yield* fs.readDirectory(registryRoot, { recursive: true });
490
- yield* Effect.forEach(existing, (relativePath) => {
491
- if (path.extname(relativePath) !== ".ts") {
492
- return Effect.void;
493
- }
494
- const normalized = path.join("registeredFunctions", relativePath);
495
- if (!expected.has(normalized)) {
496
- return Effect.gen(function* () {
497
- const absolutePath = path.join(registryRoot, relativePath);
498
- if (yield* fs.exists(absolutePath)) {
499
- yield* removePathIfExists(absolutePath);
500
- yield* logFileRemoved(absolutePath);
501
- }
502
- });
503
- }
504
- return Effect.void;
505
- });
506
- });
507
-
508
- const getGeneratedSpecPath = Effect.gen(function* () {
509
- const path = yield* Path.Path;
510
- const confectDirectory = yield* ConfectDirectory.get;
511
- const generatedSpecPath = yield* GENERATED_SPEC_PATH;
512
- return path.join(confectDirectory, generatedSpecPath);
513
- });
514
-
515
- const loadGeneratedSpec = Effect.gen(function* () {
516
- const specPath = yield* getGeneratedSpecPath;
517
- const { module: specModule } = yield* Bundler.bundle(specPath);
518
- const spec = specModule.default;
519
-
520
- if (!Spec.isSpec(spec)) {
521
- return yield* Effect.dieMessage(
522
- "_generated/spec.ts does not export a valid Spec",
523
- );
524
- }
525
-
526
- return spec;
527
- });
528
-
529
- const emptyFunctionPaths = FunctionPaths.FunctionPaths.make(HashSet.empty());
530
-
531
- export const loadPreviousFunctionPaths = Effect.gen(function* () {
532
- const fs = yield* FileSystem.FileSystem;
533
- const specPath = yield* getGeneratedSpecPath;
534
-
535
- if (!(yield* fs.exists(specPath))) {
536
- return emptyFunctionPaths;
537
- }
538
-
539
- const specEither = yield* loadGeneratedSpec.pipe(Effect.either);
540
-
541
- return Either.match(specEither, {
542
- onLeft: () => emptyFunctionPaths,
543
- onRight: (spec) => FunctionPaths.make(spec),
544
- });
545
- });
546
-
547
- /**
548
- * Remove a now-obsolete `_generated/<name>.ts` if present (and log it), for
549
- * projects upgrading from a version that still emitted it.
550
- */
551
- const removeObsoleteGeneratedFile = (fileName: string) =>
552
- Effect.gen(function* () {
553
- const fs = yield* FileSystem.FileSystem;
554
- const path = yield* Path.Path;
555
- const confectDirectory = yield* ConfectDirectory.get;
556
- const filePath = path.join(confectDirectory, "_generated", fileName);
557
-
558
- if (yield* fs.exists(filePath)) {
559
- yield* removePathIfExists(filePath);
560
- yield* logFileRemoved(filePath);
561
- }
562
- });
563
-
564
- // `_generated/api.ts` is no longer imported by generated or impl code: impls
565
- // take the database schema (`_generated/schema`) directly, and per-group
566
- // registries reference the spec type-only. Remove any stale copy.
567
- const removeGeneratedApi = removeObsoleteGeneratedFile("api.ts");
568
-
569
- // `_generated/nodeApi.ts` is obsolete for the same reason.
570
- const removeGeneratedNodeApi = removeObsoleteGeneratedFile("nodeApi.ts");
571
-
572
- const generateFunctionModules = Effect.gen(function* () {
573
- const spec = yield* loadGeneratedSpec;
574
- return yield* generateFunctions(spec);
575
- });
576
-
577
- /**
578
- * The user-authored `confect/schema.ts` is no longer supported: codegen now
579
- * owns both `_generated/schema.ts` (runtime) and `_generated/convexSchema.ts`
580
- * (deploy), derived from a single scan of `confect/tables/*.ts`. Detect a
581
- * stray file and fail with a clear migration message — leaving it in place
582
- * would silently shadow the codegen-owned `_generated/schema.ts` /
583
- * `_generated/convexSchema.ts`.
584
- */
585
- const rejectLegacySchemaFile = Effect.gen(function* () {
586
- const fs = yield* FileSystem.FileSystem;
587
- const path = yield* Path.Path;
588
- const confectDirectory = yield* ConfectDirectory.get;
589
- const legacyPath = path.join(confectDirectory, "schema.ts");
590
-
591
- if (yield* fs.exists(legacyPath)) {
592
- return yield* new LegacySchemaFileError({ schemaPath: "schema.ts" });
593
- }
594
- });
595
-
596
- /**
597
- * Surface a yellow `⚠` warning when codegen sees no tables — either the
598
- * `confect/tables/` directory is missing or it contains no `.ts` files.
599
- * Generation still succeeds (emitting an empty `DatabaseSchema` and
600
- * `defineSchema({})`), since action-only / table-free Confect backends
601
- * are legal — but the warning catches the much more common case of a
602
- * typoed directory or files placed under the wrong root.
603
- */
604
- const warnIfNoTables = (
605
- tableModules: ReadonlyArray<TableModule.TableModule>,
606
- ) =>
607
- tableModules.length === 0
608
- ? logWarn(
609
- `No tables discovered in \`confect/${TableModule.TABLES_DIRNAME}/\`. ` +
610
- `Generating an empty schema; add a \`Table.make(...)\` module under that ` +
611
- `directory unless this backend is intentionally tables-free.`,
612
- )
613
- : Effect.void;
614
-
615
- const tableModuleBindings = (
616
- tableModules: ReadonlyArray<TableModule.TableModule>,
617
- generatedFilePath: string,
618
- ) =>
619
- Effect.gen(function* () {
620
- const path = yield* Path.Path;
621
- const confectDirectory = yield* ConfectDirectory.get;
622
- const generatedDir = path.dirname(generatedFilePath);
623
-
624
- const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
625
-
626
- return yield* Effect.forEach(tableModules, (tm) =>
627
- Effect.gen(function* () {
628
- const wrapperAbsolutePath = path.join(
629
- confectDirectory,
630
- generatedTablesDirname,
631
- `${tm.tableName}.ts`,
632
- );
633
- const importPath = yield* toModuleImportPath(
634
- path.relative(generatedDir, wrapperAbsolutePath),
635
- );
636
- return {
637
- importPath,
638
- tableName: tm.tableName,
639
- };
640
- }),
641
- );
642
- });
643
-
644
- const generateIdConstructor = (
645
- tableModules: ReadonlyArray<TableModule.TableModule>,
646
- ) =>
647
- Effect.gen(function* () {
648
- const path = yield* Path.Path;
649
- const confectDirectory = yield* ConfectDirectory.get;
650
- const generatedIdPath = yield* GENERATED_ID_PATH;
651
- const idPath = path.join(confectDirectory, generatedIdPath);
652
-
653
- const tableNames = tableModules.map((tm) => tm.tableName);
654
- const contents = yield* templates.id({ tableNames });
655
-
656
- yield* writeFileStringAndLog(idPath, contents);
657
- });
658
-
659
- const generateTableWrappers = (
660
- tableModules: ReadonlyArray<TableModule.TableModule>,
661
- ) =>
662
- Effect.gen(function* () {
663
- const fs = yield* FileSystem.FileSystem;
664
- const path = yield* Path.Path;
665
- const confectDirectory = yield* ConfectDirectory.get;
666
- const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
667
- const wrappersDir = path.join(confectDirectory, generatedTablesDirname);
668
-
669
- if (!(yield* fs.exists(wrappersDir))) {
670
- yield* fs.makeDirectory(wrappersDir, { recursive: true });
671
- }
672
-
673
- yield* Effect.forEach(
674
- tableModules,
675
- (tm) =>
676
- Effect.gen(function* () {
677
- const wrapperPath = path.join(
678
- confectDirectory,
679
- generatedTablesDirname,
680
- `${tm.tableName}.ts`,
681
- );
682
- const unnamedAbsolutePath = path.join(
683
- confectDirectory,
684
- tm.relativePath,
685
- );
686
- const unnamedImportPath = yield* toModuleImportPath(
687
- path.relative(path.dirname(wrapperPath), unnamedAbsolutePath),
688
- );
689
- const contents = yield* templates.tableWrapper({
690
- tableName: tm.tableName,
691
- unnamedImportPath,
692
- });
693
- yield* writeFileStringAndLog(wrapperPath, contents);
694
- }),
695
- { concurrency: "unbounded" },
696
- );
697
- });
698
-
699
- /**
700
- * Remove any stale `_generated/tables/*.ts` wrapper whose source table
701
- * has been deleted or renamed. Mirrors `removeObsoleteRegisteredFunctions`
702
- * for the wrapper directory.
703
- */
704
- const removeObsoleteTableWrappers = (
705
- tableModules: ReadonlyArray<TableModule.TableModule>,
706
- ) =>
707
- Effect.gen(function* () {
708
- const fs = yield* FileSystem.FileSystem;
709
- const path = yield* Path.Path;
710
- const confectDirectory = yield* ConfectDirectory.get;
711
- const generatedTablesDirname = yield* GENERATED_TABLES_DIRNAME;
712
- const wrappersDir = path.join(confectDirectory, generatedTablesDirname);
713
-
714
- if (!(yield* fs.exists(wrappersDir))) {
715
- return;
716
- }
717
-
718
- const expected = new Set(tableModules.map((tm) => `${tm.tableName}.ts`));
719
- const existing = yield* fs.readDirectory(wrappersDir, { recursive: true });
720
- yield* Effect.forEach(existing, (entry) => {
721
- if (path.extname(entry) !== ".ts") {
722
- return Effect.void;
723
- }
724
- if (!expected.has(entry)) {
725
- return Effect.gen(function* () {
726
- const absolutePath = path.join(wrappersDir, entry);
727
- if (yield* fs.exists(absolutePath)) {
728
- yield* removePathIfExists(absolutePath);
729
- yield* logFileRemoved(absolutePath);
730
- }
731
- });
732
- }
733
- return Effect.void;
734
- });
735
- });
736
-
737
- const generateRuntimeSchema = (
738
- tableModules: ReadonlyArray<TableModule.TableModule>,
739
- ) =>
740
- Effect.gen(function* () {
741
- const path = yield* Path.Path;
742
- const confectDirectory = yield* ConfectDirectory.get;
743
- const generatedSchemaPath = yield* GENERATED_SCHEMA_PATH;
744
- const schemaPath = path.join(confectDirectory, generatedSchemaPath);
745
-
746
- const bindings = yield* tableModuleBindings(tableModules, schemaPath);
747
- const contents = yield* templates.runtimeSchema({ tableModules: bindings });
748
-
749
- yield* writeFileStringAndLog(schemaPath, contents);
750
- });
751
-
752
- const generateConvexSchema = (
753
- tableModules: ReadonlyArray<TableModule.TableModule>,
754
- ) =>
755
- Effect.gen(function* () {
756
- const path = yield* Path.Path;
757
- const confectDirectory = yield* ConfectDirectory.get;
758
- const generatedConvexSchemaPath = yield* GENERATED_CONVEX_SCHEMA_PATH;
759
- const convexSchemaPath = path.join(
760
- confectDirectory,
761
- generatedConvexSchemaPath,
762
- );
763
-
764
- const bindings = yield* tableModuleBindings(tableModules, convexSchemaPath);
765
- const contents = yield* templates.convexSchema({ tableModules: bindings });
766
-
767
- yield* writeFileStringAndLog(convexSchemaPath, contents);
768
- });
769
-
770
- const generateConvexSchemaReexport = Effect.gen(function* () {
771
- const path = yield* Path.Path;
772
- const confectDirectory = yield* ConfectDirectory.get;
773
- const convexDirectory = yield* ConvexDirectory.get;
774
- const generatedConvexSchemaRelativePath = yield* GENERATED_CONVEX_SCHEMA_PATH;
775
-
776
- const convexSchemaPath = path.join(convexDirectory, "schema.ts");
777
- const generatedConvexSchemaPath = path.join(
778
- confectDirectory,
779
- generatedConvexSchemaRelativePath,
780
- );
781
-
782
- const convexSchemaImportPath = yield* toModuleImportPath(
783
- path.relative(path.dirname(convexSchemaPath), generatedConvexSchemaPath),
784
- );
785
-
786
- const schemaContents = yield* templates.schema({
787
- convexSchemaImportPath,
788
- });
789
-
790
- yield* writeFileStringAndLog(convexSchemaPath, schemaContents);
791
- });
792
-
793
- const generateServices = Effect.gen(function* () {
794
- const path = yield* Path.Path;
795
- const confectDirectory = yield* ConfectDirectory.get;
796
-
797
- const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
798
-
799
- const servicesPath = path.join(confectGeneratedDirectory, "services.ts");
800
- const generatedSchemaPath = yield* GENERATED_SCHEMA_PATH;
801
- const schemaImportPath = yield* toModuleImportPath(
802
- path.relative(
803
- path.dirname(servicesPath),
804
- path.join(confectDirectory, generatedSchemaPath),
805
- ),
806
- );
807
-
808
- const servicesContentsString = yield* templates.services({
809
- schemaImportPath,
810
- });
811
-
812
- yield* writeFileStringAndLog(servicesPath, servicesContentsString);
813
- });
814
-
815
- const generateRefs = Effect.gen(function* () {
816
- const path = yield* Path.Path;
817
- const confectDirectory = yield* ConfectDirectory.get;
818
-
819
- const confectGeneratedDirectory = path.join(confectDirectory, "_generated");
820
- const refsPath = path.join(confectGeneratedDirectory, "refs.ts");
821
- const refsDir = path.dirname(refsPath);
822
- const generatedSpecPath = yield* GENERATED_SPEC_PATH;
823
-
824
- const specImportPath = yield* toModuleImportPath(
825
- path.relative(refsDir, path.join(confectDirectory, generatedSpecPath)),
826
- );
827
-
828
- const refsContents = yield* templates.refs({ specImportPath });
829
-
830
- yield* writeFileStringAndLog(refsPath, refsContents);
831
- });
832
-
833
- const logGenerated = (effect: typeof generateHttp) =>
834
- effect.pipe(
835
- Effect.tap(
836
- Option.match({
837
- onNone: () => Effect.void,
838
- onSome: ({ change, convexFilePath }) =>
839
- Match.value(change).pipe(
840
- Match.when("Added", () => logFileAdded(convexFilePath)),
841
- Match.when("Modified", () => logFileModified(convexFilePath)),
842
- Match.when("Unchanged", () => Effect.void),
843
- Match.exhaustive,
844
- ),
845
- }),
846
- ),
847
- );