@ogygie/generate 0.32.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KQ7RGIBV.js → chunk-MI725PT3.js} +646 -198
- package/dist/cli/run.js +1 -1
- package/dist/emit/db.d.ts +0 -1
- package/dist/emit/hole-tests.d.ts +2 -0
- package/dist/emit/mssql/statements.d.ts +6 -2
- package/dist/emit/plan.d.ts +47 -2
- package/dist/generation-block.schema.d.ts +76 -2
- package/dist/hole-shape.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +26 -5
- package/package.json +3 -3
|
@@ -423,6 +423,8 @@ var ReferenceSchema = z3.strictObject({
|
|
|
423
423
|
/** A nullable integer field of the read element carrying the count of references. */
|
|
424
424
|
countField: FieldNameSchema.optional()
|
|
425
425
|
});
|
|
426
|
+
var SharedTableSchema = z3.strictObject({ name: TableNameSchema, schema: SchemaNameSchema });
|
|
427
|
+
var SharedTableDeclarationSchema = z3.union([TableNameSchema, SharedTableSchema]);
|
|
426
428
|
var ContractsSchema = z3.strictObject({
|
|
427
429
|
/** Path of the schemas module, relative to the feature folder. */
|
|
428
430
|
module: z3.string().min(1),
|
|
@@ -435,9 +437,15 @@ var ContractsSchema = z3.strictObject({
|
|
|
435
437
|
key: FieldNameSchema.optional(),
|
|
436
438
|
slots: z3.record(IdentifierSchema, SlotSchema).optional(),
|
|
437
439
|
references: z3.record(IdentifierSchema, ReferenceSchema).optional(),
|
|
438
|
-
/** D32 precision (M3): tables of the product, outside the block, a view hole may read (`from`) — a referential projection, a shared table. Declared, never created nor written. */
|
|
439
|
-
sharedTables: z3.array(
|
|
440
|
+
/** D32 precision (M3): tables of the product, outside the block, a view hole may read (`from`) — a referential projection, a shared table. Declared, never created nor written. D60: declared WITH the schema of its rows, an aggregate hole reads it typed. */
|
|
441
|
+
sharedTables: z3.array(SharedTableDeclarationSchema).optional()
|
|
440
442
|
});
|
|
443
|
+
function sharedTableNameOf(declaration) {
|
|
444
|
+
return typeof declaration === "string" ? declaration : declaration.name;
|
|
445
|
+
}
|
|
446
|
+
function typedSharedTablesOf(contracts) {
|
|
447
|
+
return (contracts.sharedTables ?? []).filter((declaration) => typeof declaration !== "string");
|
|
448
|
+
}
|
|
441
449
|
var OrderDirectionSchema = z3.enum(["asc", "desc"]);
|
|
442
450
|
var OrderTupleSchema = z3.tuple([FieldNameSchema, OrderDirectionSchema]);
|
|
443
451
|
var OrderBySchema = z3.union([
|
|
@@ -582,12 +590,36 @@ var ViewHoleSchema = z3.strictObject({
|
|
|
582
590
|
value: z3.unknown()
|
|
583
591
|
})
|
|
584
592
|
});
|
|
593
|
+
var AggregateHoleSchema = z3.strictObject({
|
|
594
|
+
id: IdentifierSchema,
|
|
595
|
+
kind: z3.literal("aggregate"),
|
|
596
|
+
/** The fields of the input the hole reads — its first parameter is `Pick<input, reads>`; may be empty. */
|
|
597
|
+
reads: z3.array(FieldNameSchema).default([]),
|
|
598
|
+
/** Shared tables declared WITH a schema under contracts.sharedTables, whose rows the hole receives, typed. */
|
|
599
|
+
from: z3.array(TableNameSchema).optional(),
|
|
600
|
+
/** The root field of the read's output the hole writes. */
|
|
601
|
+
writes: FieldNameSchema,
|
|
602
|
+
/** The clock is the only effect an aggregate may declare: it receives its rows, it never reads the database. */
|
|
603
|
+
effects: z3.array(z3.literal("clock")).default([]),
|
|
604
|
+
examples: z3.strictObject({
|
|
605
|
+
/** One complete case per example: the input (its `reads`), the resource rows, the shared rows by table, the value the hole must answer. */
|
|
606
|
+
accept: z3.array(
|
|
607
|
+
z3.strictObject({
|
|
608
|
+
input: ExampleRecordSchema.optional(),
|
|
609
|
+
rows: z3.array(ExampleRecordSchema),
|
|
610
|
+
shared: z3.record(TableNameSchema, z3.array(ExampleRecordSchema)).optional(),
|
|
611
|
+
value: z3.unknown()
|
|
612
|
+
})
|
|
613
|
+
).min(1)
|
|
614
|
+
})
|
|
615
|
+
});
|
|
585
616
|
var HoleSchema = z3.discriminatedUnion("kind", [
|
|
586
617
|
ValidateHoleSchema,
|
|
587
618
|
TransformHoleSchema,
|
|
588
619
|
DeriveHoleSchema,
|
|
589
620
|
ProjectHoleSchema,
|
|
590
|
-
ViewHoleSchema
|
|
621
|
+
ViewHoleSchema,
|
|
622
|
+
AggregateHoleSchema
|
|
591
623
|
]);
|
|
592
624
|
function acceptExamplesOf(hole) {
|
|
593
625
|
return Array.isArray(hole.examples.accept) ? hole.examples.accept : [hole.examples.accept];
|
|
@@ -597,7 +629,8 @@ var HOLE_KIND_SERVES = {
|
|
|
597
629
|
transform: ["import"],
|
|
598
630
|
derive: ["create"],
|
|
599
631
|
project: ["read"],
|
|
600
|
-
view: ["read"]
|
|
632
|
+
view: ["read"],
|
|
633
|
+
aggregate: ["read"]
|
|
601
634
|
};
|
|
602
635
|
var GenerationBlockShapeSchema = z3.strictObject({
|
|
603
636
|
contracts: ContractsSchema,
|
|
@@ -793,12 +826,28 @@ function checkCrossReferences(block, issue) {
|
|
|
793
826
|
}
|
|
794
827
|
});
|
|
795
828
|
}
|
|
829
|
+
if (hole.kind === "aggregate") {
|
|
830
|
+
const typed = new Set(typedSharedTablesOf(block.contracts).map((table) => table.name));
|
|
831
|
+
for (const source of hole.from ?? []) {
|
|
832
|
+
if (!typed.has(source)) {
|
|
833
|
+
issue(["holes", index, "from"], `aggregate hole "${hole.id}" reads "${source}", which is not a shared table declared with a schema: declare it as contracts.sharedTables: [{ name: ${source}, schema: <RowSchema> }] (D60) \u2014 a bare name serves view holes only`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const reads = new Set(hole.reads);
|
|
837
|
+
hole.examples.accept.forEach((example, position) => {
|
|
838
|
+
const outside = Object.keys(example.input ?? {}).filter((key) => !reads.has(key));
|
|
839
|
+
if (outside.length > 0) issue(["holes", index, "examples"], `examples.accept[${position}].input reads ${outside.join(", ")}, which is not in reads (${hole.reads.join(", ") || "none"})`);
|
|
840
|
+
const unknownTables = Object.keys(example.shared ?? {}).filter((table) => !(hole.from ?? []).includes(table));
|
|
841
|
+
if (unknownTables.length > 0) issue(["holes", index, "examples"], `examples.accept[${position}].shared carries ${unknownTables.join(", ")}, which is not in from (${(hole.from ?? []).join(", ") || "none"})`);
|
|
842
|
+
if (!("value" in example)) issue(["holes", index, "examples"], `examples.accept[${position}] has no value: an aggregate example says what the hole answers for its rows`);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
796
845
|
if (hole.kind === "view") {
|
|
797
846
|
const known = /* @__PURE__ */ new Set([
|
|
798
847
|
...block.contracts.table === void 0 ? [] : [tableNameOf(block.contracts.table)],
|
|
799
848
|
...Object.values(slots).map((slot) => tableNameOf(slot.table)),
|
|
800
849
|
...Object.values(references).map((reference) => tableNameOf(reference.table)),
|
|
801
|
-
...block.contracts.sharedTables ?? [],
|
|
850
|
+
...(block.contracts.sharedTables ?? []).map(sharedTableNameOf),
|
|
802
851
|
...viewNames
|
|
803
852
|
]);
|
|
804
853
|
known.delete(hole.name);
|
|
@@ -1209,6 +1258,7 @@ function vocabularyOf(operation, block, guards = EMPTY_GUARD_REGISTRY, contractV
|
|
|
1209
1258
|
namedValues.add(`${hole.writes} d\xE9riv\xE9`);
|
|
1210
1259
|
if (hole.error !== void 0) codes.set(hole.error, 400);
|
|
1211
1260
|
}
|
|
1261
|
+
if (hole?.kind === "aggregate") namedValues.add(`${hole.writes} d\xE9riv\xE9`);
|
|
1212
1262
|
}
|
|
1213
1263
|
return { cases, namedValues, phrases, codes, successStatus: SUCCESS_STATUS_BY_KIND[operation.kind] };
|
|
1214
1264
|
}
|
|
@@ -2313,6 +2363,14 @@ function checkBlockAgainstSchemas(input) {
|
|
|
2313
2363
|
}
|
|
2314
2364
|
if (model !== null) checkAdoptedShape(tableRefOf(reference.table), `contracts.references.${name}.table`, model, [], issue);
|
|
2315
2365
|
}
|
|
2366
|
+
const sharedModels = /* @__PURE__ */ new Map();
|
|
2367
|
+
(block.contracts.sharedTables ?? []).forEach((declaration, position) => {
|
|
2368
|
+
if (typeof declaration === "string") return;
|
|
2369
|
+
const model = exists(declaration.schema, `contracts.sharedTables.${position}.schema`);
|
|
2370
|
+
if (model === null) return;
|
|
2371
|
+
if (model.kind !== "object") issue(`contracts.sharedTables.${position}.schema`, `${declaration.schema} is not an object schema: a shared table's schema describes one row`, "Name a z.object() schema of one row of the shared table (D60).");
|
|
2372
|
+
else sharedModels.set(declaration.name, model);
|
|
2373
|
+
});
|
|
2316
2374
|
const supersedesMotif = block.motifs.appendOnlySupersedes;
|
|
2317
2375
|
if (supersedesMotif !== void 0 && resource !== null && resource.kind === "object") {
|
|
2318
2376
|
const missing = ["supersedes", "supersededBy"].filter((field) => {
|
|
@@ -2416,6 +2474,22 @@ function checkBlockAgainstSchemas(input) {
|
|
|
2416
2474
|
);
|
|
2417
2475
|
}
|
|
2418
2476
|
}
|
|
2477
|
+
if (hole.kind === "aggregate") {
|
|
2478
|
+
const outside = hole.reads.filter((field) => !inputFields.has(field));
|
|
2479
|
+
if (outside.length > 0) issue(`holes.${holeIndex}.reads`, `aggregate hole "${hole.id}" reads ${outside.join(", ")}, not field(s) of ${operation.input}`, "An aggregate hole reads fields of the input of the read it serves (D60).");
|
|
2480
|
+
if (inputFields.has(hole.writes)) issue(`holes.${holeIndex}.writes`, `aggregate hole "${hole.id}" writes "${hole.writes}", which ${operation.input} already carries: an echo of the input is served as is, nothing to compute`, "Write a field of the output the input does not carry (D60).");
|
|
2481
|
+
hole.examples.accept.forEach((example, position) => {
|
|
2482
|
+
const rowsOutside = [...new Set(example.rows.flatMap((row) => Object.keys(row).filter((key) => !resourceFields.has(key))))];
|
|
2483
|
+
if (rowsOutside.length > 0) issue(`holes.${holeIndex}.examples`, `examples.accept[${position}].rows carry ${rowsOutside.join(", ")}, not field(s) of ${block.contracts.resource ?? "the resource"}`, "The rows of an aggregate example are rows of the resource schema, partial allowed: the test completes them (D60).");
|
|
2484
|
+
for (const [table, sharedRows] of Object.entries(example.shared ?? {})) {
|
|
2485
|
+
const model = sharedModels.get(table);
|
|
2486
|
+
if (model === void 0) continue;
|
|
2487
|
+
const fields = new Set(fieldNamesOf(model));
|
|
2488
|
+
const sharedOutside = [...new Set(sharedRows.flatMap((row) => Object.keys(row).filter((key) => !fields.has(key))))];
|
|
2489
|
+
if (sharedOutside.length > 0) issue(`holes.${holeIndex}.examples`, `examples.accept[${position}].shared.${table} carries ${sharedOutside.join(", ")}, not field(s) of its schema`, "The shared rows of an aggregate example are rows of the shared table's schema (D60).");
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2419
2493
|
if (hole.kind === "derive" && resource !== null) {
|
|
2420
2494
|
if (!resourceFields.has(hole.writes)) {
|
|
2421
2495
|
issue(`holes.${holeIndex}.writes`, `derive hole "${hole.id}" writes "${hole.writes}", which is not a field of ${block.contracts.resource}`, "A derive hole writes a field of the resource (D32).");
|
|
@@ -2497,12 +2571,29 @@ function checkOutputMapping(operation, index, output, inputFields, resource, blo
|
|
|
2497
2571
|
const entries = Object.entries(output.fields);
|
|
2498
2572
|
if (operation.kind === "read") {
|
|
2499
2573
|
const collection = readCollectionOf(output, resource);
|
|
2574
|
+
const aggregates = (operation.holes ?? []).map((id) => block.holes.find((candidate) => candidate.id === id)).flatMap((hole) => hole?.kind === "aggregate" ? [hole] : []);
|
|
2575
|
+
const aggregateWrites = new Set(aggregates.map((hole) => hole.writes));
|
|
2576
|
+
for (const hole of aggregates) {
|
|
2577
|
+
if (!(hole.writes in output.fields)) {
|
|
2578
|
+
issue(`holes.${block.holes.indexOf(hole)}.writes`, `aggregate hole "${hole.id}" writes "${hole.writes}", which is not a root field of ${operation.output}`, "An aggregate hole writes a root field of the read's output (D60).");
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2500
2581
|
if (collection === null) {
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2582
|
+
if (aggregates.length === 0) {
|
|
2583
|
+
issue(
|
|
2584
|
+
path,
|
|
2585
|
+
`${operation.output} must carry exactly one array whose element covers ${resourceLabel(resource)} \u2014 found ${entries.map(([name]) => name).join(", ")}`,
|
|
2586
|
+
"Shape a read output as { <collection>: z.array(ResourceView), ...echoed input fields, ...project fields } \u2014 or, for a report computed over the rows (an aggregate read, D60), carry no collection and write every other field with an aggregate hole: { id, kind: aggregate, reads: [input fields], writes: <field>, examples: { accept: [{ input, rows, value }] } }."
|
|
2587
|
+
);
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
const projectRootsOfReport = new Set(
|
|
2591
|
+
(operation.holes ?? []).map((id) => block.holes.find((candidate) => candidate.id === id)).flatMap((hole) => hole?.kind === "project" ? hole.writes.filter((write) => !write.includes("[]")) : [])
|
|
2505
2592
|
);
|
|
2593
|
+
const others = entries.filter(([name]) => !inputFields.has(name) && !aggregateWrites.has(name) && !projectRootsOfReport.has(name));
|
|
2594
|
+
if (others.length > 0) {
|
|
2595
|
+
issue(path, `${operation.output} carries ${others.map(([name]) => name).join(", ")}: neither an echo of the input nor written by an aggregate hole of "${operation.id}"`, "In an aggregate read (D60) every root field of the output is an echo of the input (same name) or the writes of an aggregate hole the operation lists: declare a hole { kind: aggregate, writes: <field>, \u2026 } for each computed field.");
|
|
2596
|
+
}
|
|
2506
2597
|
return;
|
|
2507
2598
|
}
|
|
2508
2599
|
const projectRoots = new Set(
|
|
@@ -2514,7 +2605,7 @@ function checkOutputMapping(operation, index, output, inputFields, resource, blo
|
|
|
2514
2605
|
const viewFields2 = new Set(
|
|
2515
2606
|
(operation.holes ?? []).map((id) => block.holes.find((candidate) => candidate.id === id)).flatMap((hole) => hole?.kind === "view" ? [hole.writes] : [])
|
|
2516
2607
|
);
|
|
2517
|
-
const rootOthers = entries.filter(([name]) => name !== collection.collection && !inputFields.has(name) && !projectRoots.has(name));
|
|
2608
|
+
const rootOthers = entries.filter(([name]) => name !== collection.collection && !inputFields.has(name) && !projectRoots.has(name) && !aggregateWrites.has(name));
|
|
2518
2609
|
if (rootOthers.length > 0) {
|
|
2519
2610
|
issue(path, `${operation.output} carries ${rootOthers.map(([name]) => name).join(", ")} at its root: neither an echo of the input nor a field a project hole writes`, "A read output's root holds the collection, echoes of the input and the fields of its project holes.");
|
|
2520
2611
|
}
|
|
@@ -3378,6 +3469,22 @@ function buildFeaturePlan(input) {
|
|
|
3378
3469
|
}
|
|
3379
3470
|
return null;
|
|
3380
3471
|
};
|
|
3472
|
+
const sharedTables = typedSharedTablesOf(block.contracts).map((declaration) => {
|
|
3473
|
+
const model = modelNamed2(schemas, declaration.schema);
|
|
3474
|
+
const fields = objectFields(model, declaration.schema);
|
|
3475
|
+
const sharedColumns = [...tenantColumns(), ...Object.entries(fields).map(([name, fieldModel2]) => columnOf(name, fieldModel2, void 0))];
|
|
3476
|
+
const typeName = typeNameOfSchema(declaration.schema);
|
|
3477
|
+
return {
|
|
3478
|
+
sqlName: declaration.name,
|
|
3479
|
+
tsName: camelOfSnake(declaration.name),
|
|
3480
|
+
schemaName: declaration.schema,
|
|
3481
|
+
typeName,
|
|
3482
|
+
rowType: `${typeName}Row`,
|
|
3483
|
+
model,
|
|
3484
|
+
fields: Object.entries(fields).map(([name, fieldModel2]) => ({ name, model: fieldModel2, dtoExpression: dtoExpressionOf(name, fieldModel2, sharedColumns.find((column) => column.key === name)?.sqlType, generation.target.database) })),
|
|
3485
|
+
columns: sharedColumns
|
|
3486
|
+
};
|
|
3487
|
+
});
|
|
3381
3488
|
const holes = block.holes.map((hole) => {
|
|
3382
3489
|
const carrier = block.operations.find((operation) => isHttpOperation(operation) && (operation.holes ?? []).includes(hole.id));
|
|
3383
3490
|
invariant(carrier !== void 0 && isHttpOperation(carrier), `hole ${hole.id} has no carrier`);
|
|
@@ -3424,6 +3531,29 @@ function buildFeaturePlan(input) {
|
|
|
3424
3531
|
if (hole.kind === "project") {
|
|
3425
3532
|
return { kind: "project", id: hole.id, hole, reads: hole.reads, writes: hole.writes, outputTypeName: typeNameOfSchema(carrier.output), accepts: hole.examples.accept };
|
|
3426
3533
|
}
|
|
3534
|
+
if (hole.kind === "aggregate") {
|
|
3535
|
+
const outputModel = modelNamed2(schemas, carrier.output);
|
|
3536
|
+
const writesModel2 = outputModel.kind === "object" ? outputModel.fields[hole.writes] : void 0;
|
|
3537
|
+
invariant(writesModel2 !== void 0, `aggregate hole ${hole.id} writes an unknown output field`);
|
|
3538
|
+
const from = (hole.from ?? []).map((name) => {
|
|
3539
|
+
const table = sharedTables.find((candidate) => candidate.sqlName === name);
|
|
3540
|
+
invariant(table !== void 0, `aggregate hole ${hole.id} reads an undeclared shared table ${name}`);
|
|
3541
|
+
return table;
|
|
3542
|
+
});
|
|
3543
|
+
return {
|
|
3544
|
+
kind: "aggregate",
|
|
3545
|
+
id: hole.id,
|
|
3546
|
+
hole,
|
|
3547
|
+
reads: hole.reads,
|
|
3548
|
+
inputTypeName: typeNameOfSchema(carrier.input),
|
|
3549
|
+
resourceTypeName: resourceType,
|
|
3550
|
+
outputTypeName: typeNameOfSchema(carrier.output),
|
|
3551
|
+
writes: hole.writes,
|
|
3552
|
+
writesModel: writesModel2,
|
|
3553
|
+
from,
|
|
3554
|
+
accepts: hole.examples.accept.map((example) => ({ input: example.input ?? {}, rows: example.rows, shared: example.shared ?? {}, value: example.value }))
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3427
3557
|
const element = elementOf(carrier);
|
|
3428
3558
|
invariant(element !== null && element.model.kind === "object", `view hole ${hole.id}: the read has no element`);
|
|
3429
3559
|
const writesModel = element.model.fields[hole.writes];
|
|
@@ -3473,6 +3603,9 @@ function buildFeaturePlan(input) {
|
|
|
3473
3603
|
else if (operation.kind === "import" && model.kind === "number") outputField = name;
|
|
3474
3604
|
else if (name in inputFields) echoFields.push(name);
|
|
3475
3605
|
}
|
|
3606
|
+
const aggregateIds = new Set(block.holes.filter((hole) => hole.kind === "aggregate").map((hole) => hole.id));
|
|
3607
|
+
const report2 = operation.kind === "read" && element === null && (operation.holes ?? []).some((id) => aggregateIds.has(id));
|
|
3608
|
+
if (report2) outputField = "";
|
|
3476
3609
|
invariant(outputField !== void 0, `${operation.output} has no mappable field`);
|
|
3477
3610
|
const opHoles = (operation.holes ?? []).map((id) => {
|
|
3478
3611
|
const hole = holeById.get(id);
|
|
@@ -3513,6 +3646,8 @@ function buildFeaturePlan(input) {
|
|
|
3513
3646
|
deriveHoles: opHoles.filter((hole) => hole.kind === "derive"),
|
|
3514
3647
|
projectHoles: opHoles.filter((hole) => hole.kind === "project"),
|
|
3515
3648
|
viewHoles: opHoles.filter((hole) => hole.kind === "view"),
|
|
3649
|
+
aggregateHoles: opHoles.filter((hole) => hole.kind === "aggregate"),
|
|
3650
|
+
report: report2,
|
|
3516
3651
|
order: orderPlanOf(operation, schemas, resourceFields),
|
|
3517
3652
|
limit: operation.kind === "read" ? operation.limit ?? null : null,
|
|
3518
3653
|
range: operation.kind === "read" ? operation.range ?? null : null,
|
|
@@ -3585,6 +3720,7 @@ function buildFeaturePlan(input) {
|
|
|
3585
3720
|
key: keyField === null || keyColumn === void 0 || keyUniqueName === null ? null : { field: keyField, column: keyColumn, uniqueName: keyUniqueName },
|
|
3586
3721
|
slots,
|
|
3587
3722
|
references,
|
|
3723
|
+
sharedTables,
|
|
3588
3724
|
signed: block.motifs.signedWrite ?? null,
|
|
3589
3725
|
unique,
|
|
3590
3726
|
referenced: (() => {
|
|
@@ -3724,6 +3860,19 @@ function holeSignatureLines(hole, plan) {
|
|
|
3724
3860
|
];
|
|
3725
3861
|
}
|
|
3726
3862
|
if (hole.kind === "view") return [];
|
|
3863
|
+
if (hole.kind === "aggregate") {
|
|
3864
|
+
const picks = hole.reads.length === 0 ? "Record<never, never>" : `Pick<${hole.inputTypeName}, ${hole.reads.map((field) => quote(field)).join(" | ")}>`;
|
|
3865
|
+
const shared = hole.from.length === 0 ? [] : [` shared: { ${hole.from.map((table) => `${table.tsName}: readonly ${table.typeName}[]`).join("; ")} },`];
|
|
3866
|
+
return [
|
|
3867
|
+
`// @hole ${hole.id}`,
|
|
3868
|
+
`${prefix} ${hole.id}(`,
|
|
3869
|
+
` input: ${picks},`,
|
|
3870
|
+
` rows: readonly ${hole.resourceTypeName}[],`,
|
|
3871
|
+
...shared,
|
|
3872
|
+
...effects,
|
|
3873
|
+
`): ${wrapResult(`${hole.outputTypeName}[${quote(hole.writes)}]`)} {`
|
|
3874
|
+
];
|
|
3875
|
+
}
|
|
3727
3876
|
return [
|
|
3728
3877
|
`// @hole ${hole.id}`,
|
|
3729
3878
|
`${prefix} ${hole.id}(input: { text: string }${effects.length === 0 ? "" : `, ${effects.map((line) => line.trim().replace(/,$/u, "")).join(", ")}`}): ${wrapResult(`HoleResult<${hole.rowTypeName}[], ${quote(hole.errorCode)}>`)} {`
|
|
@@ -3778,8 +3927,14 @@ function emitLogic(plan, existingLogic = null) {
|
|
|
3778
3927
|
for (const hole of plan.holes) {
|
|
3779
3928
|
if (hole.kind === "validate" || hole.kind === "derive") inputTypes.add(hole.inputTypeName);
|
|
3780
3929
|
if (hole.kind === "project") inputTypes.add(hole.outputTypeName);
|
|
3930
|
+
if (hole.kind === "aggregate") {
|
|
3931
|
+
if (hole.reads.length > 0) inputTypes.add(hole.inputTypeName);
|
|
3932
|
+
inputTypes.add(hole.resourceTypeName);
|
|
3933
|
+
inputTypes.add(hole.outputTypeName);
|
|
3934
|
+
for (const table of hole.from) inputTypes.add(table.typeName);
|
|
3935
|
+
}
|
|
3781
3936
|
}
|
|
3782
|
-
const needsClock = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") && hole.hole.effects.includes("clock"));
|
|
3937
|
+
const needsClock = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform" || hole.kind === "aggregate") && hole.hole.effects.includes("clock"));
|
|
3783
3938
|
const needsDb = plan.holes.some((hole) => (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") && hole.hole.effects.includes("db"));
|
|
3784
3939
|
const hasProject = plan.holes.some((hole) => hole.kind === "project");
|
|
3785
3940
|
const sections = [];
|
|
@@ -3808,7 +3963,8 @@ function emitLogic(plan, existingLogic = null) {
|
|
|
3808
3963
|
needsDb ? plan.tenant === null ? `import type { ${plan.databasePort?.typeName ?? "Database"} } from ${quote(plan.databasePort?.importPath ?? "../../db.js")};` : 'import type { TenantDb } from "@ogygie/core/tenant";' : null,
|
|
3809
3964
|
hasProject ? `import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};` : null,
|
|
3810
3965
|
inputTypes.size === 0 ? null : `import type { ${[...inputTypes].sort().join(", ")} } from "./schemas.js";`,
|
|
3811
|
-
|
|
3966
|
+
// A hole answering a HoleResult needs accept and refuse; an aggregate or a project answers a value — todo alone (D60).
|
|
3967
|
+
plan.holes.some((hole) => hole.kind === "validate" || hole.kind === "derive" || hole.kind === "transform") ? 'import { accept, refuse, todo, type HoleResult } from "./holes.runtime.gen.js";' : 'import { todo } from "./holes.runtime.gen.js";',
|
|
3812
3968
|
...hasProject ? [
|
|
3813
3969
|
"",
|
|
3814
3970
|
"/** What a project hole may read of the caller: the guard's context without its database (D32). */",
|
|
@@ -3902,6 +4058,16 @@ function drizzleViewLines(view, builders) {
|
|
|
3902
4058
|
`export type ${view.rowType} = typeof ${view.tsName}.$inferSelect;`
|
|
3903
4059
|
];
|
|
3904
4060
|
}
|
|
4061
|
+
function drizzleSharedLines(table, builders) {
|
|
4062
|
+
for (const column of table.columns) builders.add(builderOf(column));
|
|
4063
|
+
return [
|
|
4064
|
+
`/** READ-ONLY mapping of the shared table ${table.sqlName} (D60): a table of the product outside this feature, read by its aggregate holes through tenantDb; never created nor written here. */`,
|
|
4065
|
+
`export const ${table.tsName} = pgTable(${quote(table.sqlName)}, {`,
|
|
4066
|
+
...table.columns.map((column) => ` ${column.key}: ${column.drizzle},`),
|
|
4067
|
+
"});",
|
|
4068
|
+
`export type ${table.rowType} = typeof ${table.tsName}.$inferSelect;`
|
|
4069
|
+
];
|
|
4070
|
+
}
|
|
3905
4071
|
function tablesOf(plan) {
|
|
3906
4072
|
return [plan.table, ...plan.slots.map((slot) => slot.table), ...plan.references.flatMap((reference) => reference.table === null ? [] : [reference.table])];
|
|
3907
4073
|
}
|
|
@@ -3945,6 +4111,7 @@ function emitDbSchema(plan) {
|
|
|
3945
4111
|
const sections = [];
|
|
3946
4112
|
for (const table of tables) sections.push("", ...drizzleTableLines(table, builders, tablesByName));
|
|
3947
4113
|
for (const view of views) sections.push("", ...drizzleViewLines(view, builders));
|
|
4114
|
+
for (const table of plan.sharedTables) sections.push("", ...drizzleSharedLines(table, builders));
|
|
3948
4115
|
const needsSql = tables.some((table) => table.uniques.some((unique) => unique.where !== null));
|
|
3949
4116
|
const intent = wrap(
|
|
3950
4117
|
`Drizzle tables owned by the ${plan.featureId} feature, derived from ${plan.resource.schemaName} (${plan.specPath}, section G\xE9n\xE9ration): tenant-scoped by ${tenantOf(plan).column} (TS key \`${tenantOf(plan).columnKey}\`, the column tenantDb injects and filters).${plan.key === null ? "" : ` The business key ${plan.key.field} is unique per tenant (${plan.key.uniqueName}), the target of every slot and reference foreign key.`}${plan.slots.length === 0 ? "" : ` Slot tables: ${plan.slots.map((slot) => slot.table.sqlName).join(", ")} (cascade with the resource row).`}${plan.references.some((reference) => reference.table !== null) ? ` Reference tables: ${plan.references.flatMap((reference) => reference.table === null ? [] : [reference.table.sqlName]).join(", ")} (a referenced row cannot be deleted).` : ""}${views.length === 0 ? "" : ` Views ${views.map((view) => view.name).join(", ")} are read-only mappings: their SQL is in the migration.`}${plan.appendOnly === null ? "" : ` The append-only key (${plan.appendOnly.key.join(", ")}) is unique among CURRENT rows (superseded_by is null).`} SQL migration: ${plan.migrationPath}.`
|
|
@@ -4011,6 +4178,160 @@ function emitMigration(plan, existingMigration = null) {
|
|
|
4011
4178
|
return fileOf(sqlGeneratedHeader(plan.generatorVersion), ...intent, "", ...statements);
|
|
4012
4179
|
}
|
|
4013
4180
|
|
|
4181
|
+
// src/emit/hole-tests.ts
|
|
4182
|
+
function deepOverlay(base, overlay) {
|
|
4183
|
+
if (Array.isArray(overlay)) {
|
|
4184
|
+
const baseItems = Array.isArray(base) ? base : [];
|
|
4185
|
+
return overlay.map((item, index) => deepOverlay(baseItems[index] ?? baseItems[0], item));
|
|
4186
|
+
}
|
|
4187
|
+
if (overlay !== null && typeof overlay === "object") {
|
|
4188
|
+
const result = base !== null && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
|
|
4189
|
+
for (const [key, value] of Object.entries(overlay)) result[key] = deepOverlay(result[key], value);
|
|
4190
|
+
return result;
|
|
4191
|
+
}
|
|
4192
|
+
return overlay;
|
|
4193
|
+
}
|
|
4194
|
+
function sampleUnder(model, overlay) {
|
|
4195
|
+
if (model.kind === "object" && overlay !== null && typeof overlay === "object" && !Array.isArray(overlay)) {
|
|
4196
|
+
const given = overlay;
|
|
4197
|
+
const value = {};
|
|
4198
|
+
for (const [name, field] of Object.entries(model.fields)) {
|
|
4199
|
+
if (name in given) value[name] = sampleUnder(field, given[name]);
|
|
4200
|
+
else if (!field.optional) {
|
|
4201
|
+
const sample = sampleOf(field, [name]);
|
|
4202
|
+
value[name] = sample.ok ? sample.value : null;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
return value;
|
|
4206
|
+
}
|
|
4207
|
+
if (model.kind === "array" && Array.isArray(overlay)) return overlay.map((item) => sampleUnder(model.element, item));
|
|
4208
|
+
return overlay;
|
|
4209
|
+
}
|
|
4210
|
+
function projectInputOf(plan, carrier, partial) {
|
|
4211
|
+
void plan;
|
|
4212
|
+
return sampleUnder(carrier.outputModel, partial);
|
|
4213
|
+
}
|
|
4214
|
+
var HOLE_TESTS_CLOCK_INSTANT = "2026-01-01T00:00:00.000Z";
|
|
4215
|
+
function holeDeclaresClock(hole) {
|
|
4216
|
+
return (hole.hole.effects ?? []).includes("clock");
|
|
4217
|
+
}
|
|
4218
|
+
function constantNameOf(identifier) {
|
|
4219
|
+
return identifier.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
|
|
4220
|
+
}
|
|
4221
|
+
function emitHoleTests(plan) {
|
|
4222
|
+
const blocks = [];
|
|
4223
|
+
const testedHoles = plan.holes.filter((hole) => hole.kind !== "view");
|
|
4224
|
+
const needsOutputs = /* @__PURE__ */ new Set();
|
|
4225
|
+
const needsExamples = /* @__PURE__ */ new Set();
|
|
4226
|
+
const needsClock = testedHoles.some((hole) => hole.kind !== "project" && holeDeclaresClock(hole));
|
|
4227
|
+
for (const hole of testedHoles) {
|
|
4228
|
+
const withEffects = (argument) => hole.kind !== "project" && holeDeclaresClock(hole) ? `${argument}, CLOCK` : argument;
|
|
4229
|
+
const call = (argument) => isAsyncHole(hole) ? `await ${hole.id}(${withEffects(argument)})` : `${hole.id}(${withEffects(argument)})`;
|
|
4230
|
+
blocks.push("", ` describe(${quote(hole.id)}, () => {`);
|
|
4231
|
+
if (hole.kind === "validate") {
|
|
4232
|
+
hole.accepts.forEach((accept, index) => {
|
|
4233
|
+
blocks.push(
|
|
4234
|
+
` it(${quote(`accepte l'exemple ${index + 1} : ${JSON.stringify(accept)}`)}, async () => {`,
|
|
4235
|
+
` expect((${call(JSON.stringify(accept))}).ok).toBe(true);`,
|
|
4236
|
+
" });"
|
|
4237
|
+
);
|
|
4238
|
+
});
|
|
4239
|
+
blocks.push(
|
|
4240
|
+
` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}${hole.hole.suggestedFix === true ? " et un suggestedFix" : ""}`)}, async () => {`,
|
|
4241
|
+
` const result = ${call(JSON.stringify(hole.reject))};`,
|
|
4242
|
+
" expect(result.ok).toBe(false);",
|
|
4243
|
+
" if (result.ok) return;",
|
|
4244
|
+
` expect(result.code).toBe(${quote(hole.errorCode)});`,
|
|
4245
|
+
" expect(result.suggestedFix.length).toBeGreaterThan(0);",
|
|
4246
|
+
" });"
|
|
4247
|
+
);
|
|
4248
|
+
} else if (hole.kind === "derive") {
|
|
4249
|
+
hole.accepts.forEach((accept, index) => {
|
|
4250
|
+
blocks.push(
|
|
4251
|
+
` it(${quote(`d\xE9rive ${JSON.stringify(accept.output)} de l'exemple ${index + 1} : ${JSON.stringify(accept.input)}`)}, async () => {`,
|
|
4252
|
+
` const result = ${call(JSON.stringify(accept.input))};`,
|
|
4253
|
+
" expect(result.ok).toBe(true);",
|
|
4254
|
+
" if (!result.ok) return;",
|
|
4255
|
+
` expect(result.value).toEqual(${JSON.stringify(accept.output)});`,
|
|
4256
|
+
" });"
|
|
4257
|
+
);
|
|
4258
|
+
});
|
|
4259
|
+
if (hole.reject !== null && hole.errorCode !== null) {
|
|
4260
|
+
blocks.push(
|
|
4261
|
+
` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}`)}, async () => {`,
|
|
4262
|
+
` const result = ${call(JSON.stringify(hole.reject))};`,
|
|
4263
|
+
" expect(result.ok).toBe(false);",
|
|
4264
|
+
" if (result.ok) return;",
|
|
4265
|
+
` expect(result.code).toBe(${quote(hole.errorCode)});`,
|
|
4266
|
+
" expect(result.suggestedFix.length).toBeGreaterThan(0);",
|
|
4267
|
+
" });"
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
4270
|
+
} else if (hole.kind === "project") {
|
|
4271
|
+
const carrier = plan.operations.find((operation) => operation.projectHoles.some((candidate) => candidate.id === hole.id));
|
|
4272
|
+
if (carrier === void 0) throw new Error(`generation plan: project hole ${hole.id} has no carrier`);
|
|
4273
|
+
needsOutputs.add(carrier.outputSchema);
|
|
4274
|
+
hole.accepts.forEach((accept, index) => {
|
|
4275
|
+
blocks.push(
|
|
4276
|
+
` it(${quote(`forme la sortie pour le contexte ${JSON.stringify(accept.context)} (exemple ${index + 1})`)}, () => {`,
|
|
4277
|
+
` const input = ${carrier.outputSchema}.parse(${JSON.stringify(projectInputOf(plan, carrier, accept.input))});`,
|
|
4278
|
+
` expect(${hole.id}(${JSON.stringify(accept.context)}, input)).toMatchObject(${JSON.stringify(accept.output)});`,
|
|
4279
|
+
" });"
|
|
4280
|
+
);
|
|
4281
|
+
});
|
|
4282
|
+
} else if (hole.kind === "aggregate") {
|
|
4283
|
+
const constant = `EXAMPLES_OF_${constantNameOf(hole.id)}`;
|
|
4284
|
+
needsExamples.add(constant);
|
|
4285
|
+
hole.accepts.forEach((accept, index) => {
|
|
4286
|
+
blocks.push(
|
|
4287
|
+
` it(${quote(`rend ${JSON.stringify(accept.value)} pour l'exemple ${index + 1} (${accept.rows.length} ligne(s))`)}, async () => {`,
|
|
4288
|
+
` const example = ${constant}[${index}];`,
|
|
4289
|
+
' if (example === undefined) throw new Error("example missing");',
|
|
4290
|
+
` expect(${call(`example.input, example.rows${hole.from.length === 0 ? "" : ", example.shared"}`)}).toEqual(example.value);`,
|
|
4291
|
+
" });"
|
|
4292
|
+
);
|
|
4293
|
+
});
|
|
4294
|
+
} else {
|
|
4295
|
+
blocks.push(
|
|
4296
|
+
` it("accepte le fichier exemple et rend ses lignes", async () => {`,
|
|
4297
|
+
` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.accept.input)} }`)};`,
|
|
4298
|
+
" expect(result.ok).toBe(true);",
|
|
4299
|
+
" if (!result.ok) return;",
|
|
4300
|
+
` expect(result.value).toEqual(${JSON.stringify(hole.hole.examples.accept.output)});`,
|
|
4301
|
+
" });",
|
|
4302
|
+
` it(${quote(`refuse le fichier illisible avec ${hole.errorCode} et sa ligne`)}, async () => {`,
|
|
4303
|
+
` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.reject.input)} }`)};`,
|
|
4304
|
+
" expect(result.ok).toBe(false);",
|
|
4305
|
+
" if (result.ok) return;",
|
|
4306
|
+
` expect(result.code).toBe(${quote(hole.errorCode)});`,
|
|
4307
|
+
` expect(result.line).toBe(${JSON.stringify(hole.hole.examples.reject.line ?? 1)});`,
|
|
4308
|
+
" expect(result.suggestedFix.length).toBeGreaterThan(0);",
|
|
4309
|
+
" });"
|
|
4310
|
+
);
|
|
4311
|
+
}
|
|
4312
|
+
blocks.push(" });");
|
|
4313
|
+
}
|
|
4314
|
+
return fileOf(
|
|
4315
|
+
tsGeneratedHeader(plan.generatorVersion),
|
|
4316
|
+
intentBlock(
|
|
4317
|
+
wrap(
|
|
4318
|
+
`Pure tests of the holes of ${plan.featureId}, derived from their examples (D23, D32): each accept example passes, the reject example answers the hole's code. Red while a body reads todo; what a candidate body is run against (D24).${plan.holes.length !== testedHoles.length ? " The view holes are proven on the bench, not here." : ""}`
|
|
4319
|
+
)
|
|
4320
|
+
),
|
|
4321
|
+
'import { describe, expect, it } from "vitest";',
|
|
4322
|
+
needsClock ? 'import { fixedClock } from "@ogygie/core/clock";' : null,
|
|
4323
|
+
`import { ${testedHoles.map((hole) => hole.id).sort().join(", ")} } from "./logic.js";`,
|
|
4324
|
+
needsOutputs.size === 0 ? null : `import { ${[...needsOutputs].sort().join(", ")} } from "./schemas.js";`,
|
|
4325
|
+
needsExamples.size === 0 ? null : `import { ${[...needsExamples].sort().join(", ")} } from "./factories.gen.js";`,
|
|
4326
|
+
needsClock ? "" : null,
|
|
4327
|
+
needsClock ? `const CLOCK = fixedClock(${quote(HOLE_TESTS_CLOCK_INSTANT)});` : null,
|
|
4328
|
+
"",
|
|
4329
|
+
`describe(${quote(`${plan.featureId} \u2014 trous`)}, () => {`,
|
|
4330
|
+
...blocks,
|
|
4331
|
+
"});"
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4014
4335
|
// src/hole-shape.ts
|
|
4015
4336
|
import { createHash } from "node:crypto";
|
|
4016
4337
|
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -4073,7 +4394,7 @@ function normalizedTypeOf(model) {
|
|
|
4073
4394
|
return `${base}${model.nullable ? "?" : ""}`;
|
|
4074
4395
|
}
|
|
4075
4396
|
var HoleShapeSchema = z6.strictObject({
|
|
4076
|
-
kind: z6.enum(["validate", "transform", "derive", "project", "view", "custom-view"]),
|
|
4397
|
+
kind: z6.enum(["validate", "transform", "derive", "project", "view", "aggregate", "custom-view"]),
|
|
4077
4398
|
/** Normalized types of the fields read, by position. */
|
|
4078
4399
|
reads: z6.array(z6.string()),
|
|
4079
4400
|
/** Transform only: normalized types of the columns, by position. */
|
|
@@ -4107,7 +4428,7 @@ function hash16(text) {
|
|
|
4107
4428
|
}
|
|
4108
4429
|
function rowModelOf2(hole, plan) {
|
|
4109
4430
|
const carrier = plan.operations.find(
|
|
4110
|
-
(operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id)
|
|
4431
|
+
(operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id) || operation.aggregateHoles.some((candidate) => candidate.id === hole.id)
|
|
4111
4432
|
);
|
|
4112
4433
|
if (carrier === void 0) return null;
|
|
4113
4434
|
if (carrier.kind === "import") {
|
|
@@ -4143,6 +4464,9 @@ function shapeOfHole(hole, plan) {
|
|
|
4143
4464
|
if (hole.kind === "view") {
|
|
4144
4465
|
return { kind: "view", reads: hole.from.map(() => "table"), columns: hole.columns.map((column) => column.sqlType), codeRole: "none", returns: `view:${hole.shape}` };
|
|
4145
4466
|
}
|
|
4467
|
+
if (hole.kind === "aggregate") {
|
|
4468
|
+
return { kind: "aggregate", reads: readTypes(hole.reads), columns: ["rows", ...hole.from.map(() => "table")], codeRole: "none", returns: normalizedTypeOf(hole.writesModel) };
|
|
4469
|
+
}
|
|
4146
4470
|
return {
|
|
4147
4471
|
kind: "transform",
|
|
4148
4472
|
reads: ["string:text"],
|
|
@@ -4166,7 +4490,7 @@ function signatureHashOf(signature) {
|
|
|
4166
4490
|
}
|
|
4167
4491
|
function bodyTemplateOf(body, hole) {
|
|
4168
4492
|
const replacements = [];
|
|
4169
|
-
if (hole.kind === "validate" || hole.kind === "derive") {
|
|
4493
|
+
if (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "aggregate") {
|
|
4170
4494
|
hole.reads.forEach((name, index) => replacements.push([name, `{{read:${index}}}`]));
|
|
4171
4495
|
} else if (hole.kind === "transform") {
|
|
4172
4496
|
hole.columns.forEach((name, index) => replacements.push([name, `{{column:${index}}}`]));
|
|
@@ -4185,7 +4509,7 @@ function bodyTemplateOf(body, hole) {
|
|
|
4185
4509
|
}
|
|
4186
4510
|
function instantiateBody(template, hole) {
|
|
4187
4511
|
let body = template;
|
|
4188
|
-
if (hole.kind === "validate" || hole.kind === "derive") {
|
|
4512
|
+
if (hole.kind === "validate" || hole.kind === "derive" || hole.kind === "aggregate") {
|
|
4189
4513
|
hole.reads.forEach((name, index) => {
|
|
4190
4514
|
body = body.replaceAll(`{{read:${index}}}`, name);
|
|
4191
4515
|
});
|
|
@@ -4235,143 +4559,6 @@ function libraryFileNameOf(entry) {
|
|
|
4235
4559
|
return `${entry.signatureHash}.json`;
|
|
4236
4560
|
}
|
|
4237
4561
|
|
|
4238
|
-
// src/emit/hole-tests.ts
|
|
4239
|
-
function deepOverlay(base, overlay) {
|
|
4240
|
-
if (Array.isArray(overlay)) {
|
|
4241
|
-
const baseItems = Array.isArray(base) ? base : [];
|
|
4242
|
-
return overlay.map((item, index) => deepOverlay(baseItems[index] ?? baseItems[0], item));
|
|
4243
|
-
}
|
|
4244
|
-
if (overlay !== null && typeof overlay === "object") {
|
|
4245
|
-
const result = base !== null && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
|
|
4246
|
-
for (const [key, value] of Object.entries(overlay)) result[key] = deepOverlay(result[key], value);
|
|
4247
|
-
return result;
|
|
4248
|
-
}
|
|
4249
|
-
return overlay;
|
|
4250
|
-
}
|
|
4251
|
-
function sampleUnder(model, overlay) {
|
|
4252
|
-
if (model.kind === "object" && overlay !== null && typeof overlay === "object" && !Array.isArray(overlay)) {
|
|
4253
|
-
const given = overlay;
|
|
4254
|
-
const value = {};
|
|
4255
|
-
for (const [name, field] of Object.entries(model.fields)) {
|
|
4256
|
-
if (name in given) value[name] = sampleUnder(field, given[name]);
|
|
4257
|
-
else if (!field.optional) {
|
|
4258
|
-
const sample = sampleOf(field, [name]);
|
|
4259
|
-
value[name] = sample.ok ? sample.value : null;
|
|
4260
|
-
}
|
|
4261
|
-
}
|
|
4262
|
-
return value;
|
|
4263
|
-
}
|
|
4264
|
-
if (model.kind === "array" && Array.isArray(overlay)) return overlay.map((item) => sampleUnder(model.element, item));
|
|
4265
|
-
return overlay;
|
|
4266
|
-
}
|
|
4267
|
-
function projectInputOf(plan, carrier, partial) {
|
|
4268
|
-
void plan;
|
|
4269
|
-
return sampleUnder(carrier.outputModel, partial);
|
|
4270
|
-
}
|
|
4271
|
-
var HOLE_TESTS_CLOCK_INSTANT = "2026-01-01T00:00:00.000Z";
|
|
4272
|
-
function holeDeclaresClock(hole) {
|
|
4273
|
-
return (hole.hole.effects ?? []).includes("clock");
|
|
4274
|
-
}
|
|
4275
|
-
function emitHoleTests(plan) {
|
|
4276
|
-
const blocks = [];
|
|
4277
|
-
const testedHoles = plan.holes.filter((hole) => hole.kind !== "view");
|
|
4278
|
-
const needsOutputs = /* @__PURE__ */ new Set();
|
|
4279
|
-
const needsClock = testedHoles.some((hole) => hole.kind !== "project" && holeDeclaresClock(hole));
|
|
4280
|
-
for (const hole of testedHoles) {
|
|
4281
|
-
const withEffects = (argument) => hole.kind !== "project" && holeDeclaresClock(hole) ? `${argument}, CLOCK` : argument;
|
|
4282
|
-
const call = (argument) => isAsyncHole(hole) ? `await ${hole.id}(${withEffects(argument)})` : `${hole.id}(${withEffects(argument)})`;
|
|
4283
|
-
blocks.push("", ` describe(${quote(hole.id)}, () => {`);
|
|
4284
|
-
if (hole.kind === "validate") {
|
|
4285
|
-
hole.accepts.forEach((accept, index) => {
|
|
4286
|
-
blocks.push(
|
|
4287
|
-
` it(${quote(`accepte l'exemple ${index + 1} : ${JSON.stringify(accept)}`)}, async () => {`,
|
|
4288
|
-
` expect((${call(JSON.stringify(accept))}).ok).toBe(true);`,
|
|
4289
|
-
" });"
|
|
4290
|
-
);
|
|
4291
|
-
});
|
|
4292
|
-
blocks.push(
|
|
4293
|
-
` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}${hole.hole.suggestedFix === true ? " et un suggestedFix" : ""}`)}, async () => {`,
|
|
4294
|
-
` const result = ${call(JSON.stringify(hole.reject))};`,
|
|
4295
|
-
" expect(result.ok).toBe(false);",
|
|
4296
|
-
" if (result.ok) return;",
|
|
4297
|
-
` expect(result.code).toBe(${quote(hole.errorCode)});`,
|
|
4298
|
-
" expect(result.suggestedFix.length).toBeGreaterThan(0);",
|
|
4299
|
-
" });"
|
|
4300
|
-
);
|
|
4301
|
-
} else if (hole.kind === "derive") {
|
|
4302
|
-
hole.accepts.forEach((accept, index) => {
|
|
4303
|
-
blocks.push(
|
|
4304
|
-
` it(${quote(`d\xE9rive ${JSON.stringify(accept.output)} de l'exemple ${index + 1} : ${JSON.stringify(accept.input)}`)}, async () => {`,
|
|
4305
|
-
` const result = ${call(JSON.stringify(accept.input))};`,
|
|
4306
|
-
" expect(result.ok).toBe(true);",
|
|
4307
|
-
" if (!result.ok) return;",
|
|
4308
|
-
` expect(result.value).toEqual(${JSON.stringify(accept.output)});`,
|
|
4309
|
-
" });"
|
|
4310
|
-
);
|
|
4311
|
-
});
|
|
4312
|
-
if (hole.reject !== null && hole.errorCode !== null) {
|
|
4313
|
-
blocks.push(
|
|
4314
|
-
` it(${quote(`refuse ${JSON.stringify(hole.reject)} avec ${hole.errorCode}`)}, async () => {`,
|
|
4315
|
-
` const result = ${call(JSON.stringify(hole.reject))};`,
|
|
4316
|
-
" expect(result.ok).toBe(false);",
|
|
4317
|
-
" if (result.ok) return;",
|
|
4318
|
-
` expect(result.code).toBe(${quote(hole.errorCode)});`,
|
|
4319
|
-
" expect(result.suggestedFix.length).toBeGreaterThan(0);",
|
|
4320
|
-
" });"
|
|
4321
|
-
);
|
|
4322
|
-
}
|
|
4323
|
-
} else if (hole.kind === "project") {
|
|
4324
|
-
const carrier = plan.operations.find((operation) => operation.projectHoles.some((candidate) => candidate.id === hole.id));
|
|
4325
|
-
if (carrier === void 0) throw new Error(`generation plan: project hole ${hole.id} has no carrier`);
|
|
4326
|
-
needsOutputs.add(carrier.outputSchema);
|
|
4327
|
-
hole.accepts.forEach((accept, index) => {
|
|
4328
|
-
blocks.push(
|
|
4329
|
-
` it(${quote(`forme la sortie pour le contexte ${JSON.stringify(accept.context)} (exemple ${index + 1})`)}, () => {`,
|
|
4330
|
-
` const input = ${carrier.outputSchema}.parse(${JSON.stringify(projectInputOf(plan, carrier, accept.input))});`,
|
|
4331
|
-
` expect(${hole.id}(${JSON.stringify(accept.context)}, input)).toMatchObject(${JSON.stringify(accept.output)});`,
|
|
4332
|
-
" });"
|
|
4333
|
-
);
|
|
4334
|
-
});
|
|
4335
|
-
} else {
|
|
4336
|
-
blocks.push(
|
|
4337
|
-
` it("accepte le fichier exemple et rend ses lignes", async () => {`,
|
|
4338
|
-
` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.accept.input)} }`)};`,
|
|
4339
|
-
" expect(result.ok).toBe(true);",
|
|
4340
|
-
" if (!result.ok) return;",
|
|
4341
|
-
` expect(result.value).toEqual(${JSON.stringify(hole.hole.examples.accept.output)});`,
|
|
4342
|
-
" });",
|
|
4343
|
-
` it(${quote(`refuse le fichier illisible avec ${hole.errorCode} et sa ligne`)}, async () => {`,
|
|
4344
|
-
` const result = ${call(`{ text: ${JSON.stringify(hole.hole.examples.reject.input)} }`)};`,
|
|
4345
|
-
" expect(result.ok).toBe(false);",
|
|
4346
|
-
" if (result.ok) return;",
|
|
4347
|
-
` expect(result.code).toBe(${quote(hole.errorCode)});`,
|
|
4348
|
-
` expect(result.line).toBe(${JSON.stringify(hole.hole.examples.reject.line ?? 1)});`,
|
|
4349
|
-
" expect(result.suggestedFix.length).toBeGreaterThan(0);",
|
|
4350
|
-
" });"
|
|
4351
|
-
);
|
|
4352
|
-
}
|
|
4353
|
-
blocks.push(" });");
|
|
4354
|
-
}
|
|
4355
|
-
return fileOf(
|
|
4356
|
-
tsGeneratedHeader(plan.generatorVersion),
|
|
4357
|
-
intentBlock(
|
|
4358
|
-
wrap(
|
|
4359
|
-
`Pure tests of the holes of ${plan.featureId}, derived from their examples (D23, D32): each accept example passes, the reject example answers the hole's code. Red while a body reads todo; what a candidate body is run against (D24).${plan.holes.length !== testedHoles.length ? " The view holes are proven on the bench, not here." : ""}`
|
|
4360
|
-
)
|
|
4361
|
-
),
|
|
4362
|
-
'import { describe, expect, it } from "vitest";',
|
|
4363
|
-
needsClock ? 'import { fixedClock } from "@ogygie/core/clock";' : null,
|
|
4364
|
-
`import { ${testedHoles.map((hole) => hole.id).sort().join(", ")} } from "./logic.js";`,
|
|
4365
|
-
needsOutputs.size === 0 ? null : `import { ${[...needsOutputs].sort().join(", ")} } from "./schemas.js";`,
|
|
4366
|
-
needsClock ? "" : null,
|
|
4367
|
-
needsClock ? `const CLOCK = fixedClock(${quote(HOLE_TESTS_CLOCK_INSTANT)});` : null,
|
|
4368
|
-
"",
|
|
4369
|
-
`describe(${quote(`${plan.featureId} \u2014 trous`)}, () => {`,
|
|
4370
|
-
...blocks,
|
|
4371
|
-
"});"
|
|
4372
|
-
);
|
|
4373
|
-
}
|
|
4374
|
-
|
|
4375
4562
|
// src/emit/mssql/sql-types.ts
|
|
4376
4563
|
function sqlColumnName(plan, column) {
|
|
4377
4564
|
return plan.target.naming === "pascal" ? column.columnName.charAt(0).toUpperCase() + column.columnName.slice(1) : column.columnName;
|
|
@@ -4647,6 +4834,33 @@ function mssqlStatements(plan) {
|
|
|
4647
4834
|
parameters: []
|
|
4648
4835
|
});
|
|
4649
4836
|
}
|
|
4837
|
+
for (const table of plan.sharedTables) {
|
|
4838
|
+
statements.push({
|
|
4839
|
+
key: `select${table.typeName}Rows`,
|
|
4840
|
+
kind: "selectShared",
|
|
4841
|
+
text: `SELECT ${selectList(plan, "t", table.columns)} FROM ${qualifiedTable(plan, table.sqlName)} AS t`,
|
|
4842
|
+
table: table.sqlName,
|
|
4843
|
+
rowType: table.rowType,
|
|
4844
|
+
parameters: []
|
|
4845
|
+
});
|
|
4846
|
+
}
|
|
4847
|
+
const arrangingRows = plan.operations.every((operation) => operation.kind !== "create") && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
|
|
4848
|
+
if (arrangingRows) {
|
|
4849
|
+
const arrangeOf = (sqlName, columns, rowType, typeName) => {
|
|
4850
|
+
const written = [...columns.filter((column) => column.key === "id"), ...columns.filter((column) => column.key !== "id")];
|
|
4851
|
+
return {
|
|
4852
|
+
key: `arrange${typeName}Row`,
|
|
4853
|
+
kind: "insert",
|
|
4854
|
+
text: `INSERT INTO ${qualifiedTable(plan, sqlName)} (${written.map((column) => sqlColumnName(plan, column)).join(", ")}) OUTPUT ${outputList(plan, "INSERTED", columns)} VALUES (${written.map((column) => `@${column.key}`).join(", ")})`,
|
|
4855
|
+
table: sqlName,
|
|
4856
|
+
rowType,
|
|
4857
|
+
parameters: written.map((column) => column.key),
|
|
4858
|
+
written: written.map((column) => column.key)
|
|
4859
|
+
};
|
|
4860
|
+
};
|
|
4861
|
+
statements.push(arrangeOf(resource.sqlName, resource.columns, resource.rowType, plan.resource.typeName));
|
|
4862
|
+
for (const table of plan.sharedTables) statements.push(arrangeOf(table.sqlName, table.columns, table.rowType, table.typeName));
|
|
4863
|
+
}
|
|
4650
4864
|
if (keyField !== null && keyColumn !== null) {
|
|
4651
4865
|
const keyName = sqlColumnName(plan, keyColumn);
|
|
4652
4866
|
statements.push({
|
|
@@ -4812,6 +5026,10 @@ function rowInterfaceLines(name, columns) {
|
|
|
4812
5026
|
function parametersLiteral(entries) {
|
|
4813
5027
|
return `{ ${entries.map(([name, expression]) => `${name}: ${expression}`).join(", ")} }`;
|
|
4814
5028
|
}
|
|
5029
|
+
function parameterValue(column, expression) {
|
|
5030
|
+
if (column.sqlType === "timestamptz") return expression;
|
|
5031
|
+
return expression;
|
|
5032
|
+
}
|
|
4815
5033
|
function notFoundLines(plan, codeExpression, indent) {
|
|
4816
5034
|
const key = plan.key?.field ?? "id";
|
|
4817
5035
|
return [
|
|
@@ -4851,6 +5069,34 @@ function viewReadLines(plan, view, statement) {
|
|
|
4851
5069
|
"}"
|
|
4852
5070
|
];
|
|
4853
5071
|
}
|
|
5072
|
+
function sharedReadLines(plan, table, statement) {
|
|
5073
|
+
return [
|
|
5074
|
+
`/** The rows of the shared table ${table.sqlName} (D60) \u2014 read-only, the product owns it. */`,
|
|
5075
|
+
`export async function ${statement.key}(db: ${plan.databasePort?.typeName ?? "Database"}): Promise<${table.rowType}[]> {`,
|
|
5076
|
+
` return db.query<${table.rowType}>(SQL.${statement.key});`,
|
|
5077
|
+
"}"
|
|
5078
|
+
];
|
|
5079
|
+
}
|
|
5080
|
+
function arrangeLines(plan, statement, typeName, columns, withId) {
|
|
5081
|
+
const written = statement.written ?? [];
|
|
5082
|
+
const valueOf = (field) => {
|
|
5083
|
+
if (field === "id") return "crypto.randomUUID()";
|
|
5084
|
+
const column = columns.find((candidate) => candidate.key === field);
|
|
5085
|
+
return column === void 0 ? `row.${field}` : parameterValue(column, `row.${field}`);
|
|
5086
|
+
};
|
|
5087
|
+
return [
|
|
5088
|
+
`/** TESTS ONLY (D60): rows of ${statement.table} arranged at their schema \u2014 the feature never writes them, they come from elsewhere. */`,
|
|
5089
|
+
`export async function arrange${typeName}Rows(db: ${plan.databasePort?.typeName ?? "Database"}, rows: readonly ${withId ? `Omit<${typeName}, "id">` : typeName}[]): Promise<${statement.rowType}[]> {`,
|
|
5090
|
+
` const inserted: ${statement.rowType}[] = [];`,
|
|
5091
|
+
" for (const row of rows) {",
|
|
5092
|
+
` const [written] = await db.query<${statement.rowType}>(SQL.${statement.key}, ${parametersLiteral(written.map((field) => [field, valueOf(field)]))});`,
|
|
5093
|
+
" if (written === undefined) throwOgygieError(insertReturnedNothing());",
|
|
5094
|
+
" inserted.push(written);",
|
|
5095
|
+
" }",
|
|
5096
|
+
" return inserted;",
|
|
5097
|
+
"}"
|
|
5098
|
+
];
|
|
5099
|
+
}
|
|
4854
5100
|
function referenceLines(plan, table, column, countStatement, insertStatement) {
|
|
4855
5101
|
const database = plan.databasePort?.typeName ?? "Database";
|
|
4856
5102
|
const typeName = table.rowType.replace(/Row$/u, "");
|
|
@@ -5061,6 +5307,7 @@ function emitMssqlRepository(plan) {
|
|
|
5061
5307
|
for (const slot of plan.slots) rows.push("", ...rowInterfaceLines(slot.table.rowType, slot.table.columns));
|
|
5062
5308
|
for (const reference of plan.references) if (reference.table !== null) rows.push("", ...rowInterfaceLines(reference.table.rowType, reference.table.columns));
|
|
5063
5309
|
for (const view of views) rows.push("", ...rowInterfaceLines(view.rowType, view.columns));
|
|
5310
|
+
for (const table of plan.sharedTables) rows.push("", ...rowInterfaceLines(table.rowType, table.columns));
|
|
5064
5311
|
const sqlLines = ["/** Every statement of this feature, `@named` parameters, read back under camelCase aliases \u2014 the stub answers these very texts (D33). */", "export const SQL = {", ...statements.map((statement) => ` ${statement.key}: ${quote(statement.text)},`), "} as const;"];
|
|
5065
5312
|
const sections = [];
|
|
5066
5313
|
for (const operation of plan.operations.filter((candidate) => candidate.kind === "read")) sections.push("", ...readFunctionLines(plan, operation, statementNamed(statements, `select${operation.pascalId}`)));
|
|
@@ -5071,8 +5318,14 @@ function emitMssqlRepository(plan) {
|
|
|
5071
5318
|
sections.push("", ...referenceLines(plan, reference.table, reference.column, statementNamed(statements, `count${typeName}`), statementNamed(statements, `insert${typeName}`)));
|
|
5072
5319
|
}
|
|
5073
5320
|
for (const view of views) sections.push("", ...viewReadLines(plan, view, statementNamed(statements, `select${view.rowType.replace(/Row$/u, "")}`)));
|
|
5074
|
-
|
|
5321
|
+
for (const table of plan.sharedTables) sections.push("", ...sharedReadLines(plan, table, statementNamed(statements, `select${table.typeName}Rows`)));
|
|
5075
5322
|
const create = plan.operations.find((operation) => operation.kind === "create");
|
|
5323
|
+
const arranging = create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
|
|
5324
|
+
if (arranging) {
|
|
5325
|
+
sections.push("", ...arrangeLines(plan, statementNamed(statements, `arrange${plan.resource.typeName}Row`), plan.resource.typeName, plan.table.columns, true));
|
|
5326
|
+
for (const table of plan.sharedTables) sections.push("", ...arrangeLines(plan, statementNamed(statements, `arrange${table.typeName}Row`), table.typeName, table.columns, false));
|
|
5327
|
+
}
|
|
5328
|
+
if (plan.key !== null) sections.push("", ...loadByKeyLines(plan, statementNamed(statements, `load${plan.resource.typeName}ByKey`)));
|
|
5076
5329
|
const update = plan.operations.find((operation) => operation.kind === "update");
|
|
5077
5330
|
const setOrClear = plan.operations.find((operation) => operation.kind === "setOrClear");
|
|
5078
5331
|
const remove = plan.operations.find((operation) => operation.kind === "delete");
|
|
@@ -5081,7 +5334,7 @@ function emitMssqlRepository(plan) {
|
|
|
5081
5334
|
if (setOrClear !== void 0) sections.push("", ...setOrClearFunctionLines(plan, setOrClear, statements));
|
|
5082
5335
|
if (remove !== void 0) sections.push("", ...deleteFunctionLines(plan, remove, statements));
|
|
5083
5336
|
const inputTypes = [...new Set([create?.inputType, update?.inputType].filter((name) => name !== void 0))].sort();
|
|
5084
|
-
const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
|
|
5337
|
+
const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...arranging ? [plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)] : [], ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
|
|
5085
5338
|
return fileOf(
|
|
5086
5339
|
tsGeneratedHeader(plan.generatorVersion),
|
|
5087
5340
|
intentBlock(
|
|
@@ -5162,6 +5415,8 @@ function emitMssqlStub(plan) {
|
|
|
5162
5415
|
const tables = tablesOf(plan);
|
|
5163
5416
|
const views = plan.holes.filter((hole) => hole.kind === "view");
|
|
5164
5417
|
const tableOf = (sqlName) => {
|
|
5418
|
+
const shared = plan.sharedTables.find((candidate) => candidate.sqlName === sqlName);
|
|
5419
|
+
if (shared !== void 0) return { sqlName: shared.sqlName, tsName: shared.tsName, rowType: shared.rowType, newType: `New${shared.typeName}`, mode: "adopted", hasId: false, primaryKey: [], alter: [], columns: shared.columns, indexes: [], uniques: [], foreignKeys: [] };
|
|
5165
5420
|
const table = tables.find((candidate) => candidate.sqlName === sqlName);
|
|
5166
5421
|
if (table === void 0) throw new Error(`generation plan: stub has no table ${sqlName}`);
|
|
5167
5422
|
return table;
|
|
@@ -5181,7 +5436,8 @@ function emitMssqlStub(plan) {
|
|
|
5181
5436
|
cases.push(` const rows = tables.${statement.table};`, ...compareLines(statement));
|
|
5182
5437
|
break;
|
|
5183
5438
|
}
|
|
5184
|
-
case "selectView":
|
|
5439
|
+
case "selectView":
|
|
5440
|
+
case "selectShared": {
|
|
5185
5441
|
rowTypes.add(statement.rowType);
|
|
5186
5442
|
cases.push(` return [...tables.${statement.table}];`);
|
|
5187
5443
|
break;
|
|
@@ -5316,9 +5572,9 @@ function emitMssqlStub(plan) {
|
|
|
5316
5572
|
}
|
|
5317
5573
|
cases.push(" }");
|
|
5318
5574
|
}
|
|
5319
|
-
const tableFields = [...tables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`), ...views.map((view) => ` ${view.name}: ${view.rowType}[];`)];
|
|
5320
|
-
const emptyFields = [...tables.map((table) => `${table.sqlName}: []`), ...views.map((view) => `${view.name}: []`)];
|
|
5321
|
-
const importedRowTypes = [.../* @__PURE__ */ new Set([...tables.map((table) => table.rowType), ...views.map((view) => view.rowType)])].sort();
|
|
5575
|
+
const tableFields = [...tables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`), ...views.map((view) => ` ${view.name}: ${view.rowType}[];`), ...plan.sharedTables.map((table) => ` ${table.sqlName}: ${table.rowType}[];`)];
|
|
5576
|
+
const emptyFields = [...tables.map((table) => `${table.sqlName}: []`), ...views.map((view) => `${view.name}: []`), ...plan.sharedTables.map((table) => `${table.sqlName}: []`)];
|
|
5577
|
+
const importedRowTypes = [.../* @__PURE__ */ new Set([...tables.map((table) => table.rowType), ...views.map((view) => view.rowType), ...plan.sharedTables.map((table) => table.rowType)])].sort();
|
|
5322
5578
|
return fileOf(
|
|
5323
5579
|
tsGeneratedHeader(plan.generatorVersion),
|
|
5324
5580
|
intentBlock(
|
|
@@ -5428,8 +5684,9 @@ function sampleOrThrow(model, variant, label) {
|
|
|
5428
5684
|
}
|
|
5429
5685
|
return sample.value;
|
|
5430
5686
|
}
|
|
5431
|
-
function acceptOverlay(holes, derives, variant) {
|
|
5687
|
+
function acceptOverlay(holes, derives, variant, aggregates = []) {
|
|
5432
5688
|
const overlay = {};
|
|
5689
|
+
for (const hole of aggregates) Object.assign(overlay, hole.accepts[Math.min(variant, hole.accepts.length - 1)]?.input ?? {});
|
|
5433
5690
|
for (const hole of holes) {
|
|
5434
5691
|
const example = hole.accepts[Math.min(variant, hole.accepts.length - 1)] ?? {};
|
|
5435
5692
|
Object.assign(overlay, example);
|
|
@@ -5443,7 +5700,8 @@ function acceptOverlay(holes, derives, variant) {
|
|
|
5443
5700
|
function validInputsOf(plan, operation) {
|
|
5444
5701
|
const holes = operation.kind === "import" ? [] : operation.validateHoles;
|
|
5445
5702
|
const derives = operation.deriveHoles;
|
|
5446
|
-
const
|
|
5703
|
+
const aggregates = operation.aggregateHoles;
|
|
5704
|
+
const variants = Math.max(1, ...holes.map((hole) => hole.accepts.length), ...derives.map((hole) => hole.accepts.length), ...aggregates.map((hole) => hole.accepts.length));
|
|
5447
5705
|
if (operation.kind === "update" && operation.inputModel.kind === "object") {
|
|
5448
5706
|
return Array.from({ length: variants }, (_, variant) => {
|
|
5449
5707
|
const body = {};
|
|
@@ -5455,7 +5713,7 @@ function validInputsOf(plan, operation) {
|
|
|
5455
5713
|
});
|
|
5456
5714
|
}
|
|
5457
5715
|
const base = sampleOrThrow(operation.inputModel, 0, operation.inputSchema);
|
|
5458
|
-
return Array.from({ length: variants }, (_, variant) => ({ ...base, ...acceptOverlay(holes, derives, variant) }));
|
|
5716
|
+
return Array.from({ length: variants }, (_, variant) => ({ ...base, ...acceptOverlay(holes, derives, variant, aggregates) }));
|
|
5459
5717
|
}
|
|
5460
5718
|
function derivedOf(operation, variant = 0) {
|
|
5461
5719
|
const derived = {};
|
|
@@ -5490,7 +5748,7 @@ function sampleOfSqlType(sqlType) {
|
|
|
5490
5748
|
if (sqlType === "uuid") return "00000000-0000-4000-8000-000000000001";
|
|
5491
5749
|
return "arranged";
|
|
5492
5750
|
}
|
|
5493
|
-
function
|
|
5751
|
+
function constantNameOf2(typeName) {
|
|
5494
5752
|
return typeName.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
|
|
5495
5753
|
}
|
|
5496
5754
|
function sampleValueOf(model, variant = 0) {
|
|
@@ -5511,13 +5769,13 @@ function emitFactories(plan) {
|
|
|
5511
5769
|
sections.push(
|
|
5512
5770
|
"",
|
|
5513
5771
|
`/** Structurally invalid ${operation.inputType}: the required field \`${invalidImport.removedField}\` is missing. */`,
|
|
5514
|
-
`export const INVALID_${
|
|
5772
|
+
`export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalidImport.value)};`
|
|
5515
5773
|
);
|
|
5516
5774
|
}
|
|
5517
5775
|
continue;
|
|
5518
5776
|
}
|
|
5519
5777
|
typeImports.add(operation.inputType);
|
|
5520
|
-
const constant = `VALID_${
|
|
5778
|
+
const constant = `VALID_${constantNameOf2(operation.inputType)}S`;
|
|
5521
5779
|
const factory = `valid${operation.inputType}`;
|
|
5522
5780
|
sections.push(
|
|
5523
5781
|
"",
|
|
@@ -5537,13 +5795,13 @@ function emitFactories(plan) {
|
|
|
5537
5795
|
sections.push(
|
|
5538
5796
|
"",
|
|
5539
5797
|
`/** Structurally invalid ${operation.inputType}: the required field \`${invalid.removedField}\` is missing. */`,
|
|
5540
|
-
`export const INVALID_${
|
|
5798
|
+
`export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = ${JSON.stringify(invalid.value)};`
|
|
5541
5799
|
);
|
|
5542
5800
|
} else if (operation.kind === "update") {
|
|
5543
5801
|
sections.push(
|
|
5544
5802
|
"",
|
|
5545
5803
|
`/** Structurally invalid ${operation.inputType}: a partial body with no field at all \u2014 the contract refuses it. */`,
|
|
5546
|
-
`export const INVALID_${
|
|
5804
|
+
`export const INVALID_${constantNameOf2(operation.inputType)}: Record<string, unknown> = {};`
|
|
5547
5805
|
);
|
|
5548
5806
|
}
|
|
5549
5807
|
}
|
|
@@ -5575,6 +5833,31 @@ function emitFactories(plan) {
|
|
|
5575
5833
|
} else if (create !== null) {
|
|
5576
5834
|
sections.push("", "/** No derive hole: an arranged row carries nothing beyond its input. */", "export const DERIVED_OF_VALID_CREATES: readonly Record<string, unknown>[] = [{}];");
|
|
5577
5835
|
}
|
|
5836
|
+
for (const hole of plan.holes) {
|
|
5837
|
+
if (hole.kind !== "aggregate") continue;
|
|
5838
|
+
const carrier = plan.operations.find((operation) => operation.aggregateHoles.some((candidate) => candidate.id === hole.id));
|
|
5839
|
+
if (carrier === void 0) continue;
|
|
5840
|
+
typeImports.add(hole.resourceTypeName);
|
|
5841
|
+
typeImports.add(hole.outputTypeName);
|
|
5842
|
+
if (hole.reads.length > 0) typeImports.add(hole.inputTypeName);
|
|
5843
|
+
for (const table of hole.from) typeImports.add(table.typeName);
|
|
5844
|
+
const inputType = hole.reads.length === 0 ? "Record<never, never>" : `Pick<${hole.inputTypeName}, ${hole.reads.map(quote).join(" | ")}>`;
|
|
5845
|
+
const sharedType = hole.from.length === 0 ? "Record<never, never>" : `{ ${hole.from.map((table) => `${table.tsName}: readonly ${table.typeName}[]`).join("; ")} }`;
|
|
5846
|
+
const examples = hole.accepts.map((accept) => {
|
|
5847
|
+
const fullInput = sampleUnder(carrier.inputModel, accept.input);
|
|
5848
|
+
const input = Object.fromEntries(hole.reads.map((field) => [field, fullInput[field]]));
|
|
5849
|
+
const rows = accept.rows.map((row) => sampleUnder(plan.resource.model, row));
|
|
5850
|
+
const shared = Object.fromEntries(hole.from.map((table) => [table.tsName, (accept.shared[table.sqlName] ?? []).map((row) => sampleUnder(table.model, row))]));
|
|
5851
|
+
return { input, rows, shared, value: accept.value };
|
|
5852
|
+
});
|
|
5853
|
+
sections.push(
|
|
5854
|
+
"",
|
|
5855
|
+
`/** The complete examples of the aggregate hole ${hole.id} (D60): input pick, rows and shared rows completed under their schemas, the value expected \u2014 one fixture for the pure test and the behaviour test. */`,
|
|
5856
|
+
`export const EXAMPLES_OF_${constantNameOf(hole.id)}: readonly { input: ${inputType}; rows: readonly ${hole.resourceTypeName}[]; shared: ${sharedType}; value: ${hole.outputTypeName}[${quote(hole.writes)}] }[] = [`,
|
|
5857
|
+
...examples.map((example) => ` ${JSON.stringify(example)},`),
|
|
5858
|
+
"];"
|
|
5859
|
+
);
|
|
5860
|
+
}
|
|
5578
5861
|
for (const reference of plan.references) {
|
|
5579
5862
|
if (reference.table === null) continue;
|
|
5580
5863
|
const model = reference.schemaName === null ? void 0 : plan.schemaModels[reference.schemaName];
|
|
@@ -5640,14 +5923,14 @@ function emitFactories(plan) {
|
|
|
5640
5923
|
sections.push(
|
|
5641
5924
|
"",
|
|
5642
5925
|
`/** The input ${hole.id} rejects (its reject example), as an overlay of a valid input. */`,
|
|
5643
|
-
`export const REJECT_EXAMPLE_OF_${
|
|
5926
|
+
`export const REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
|
|
5644
5927
|
);
|
|
5645
5928
|
}
|
|
5646
5929
|
if (hole.kind === "derive" && hole.reject !== null) {
|
|
5647
5930
|
sections.push(
|
|
5648
5931
|
"",
|
|
5649
5932
|
`/** The input ${hole.id} refuses to derive from (its reject example), as an overlay of a valid input. */`,
|
|
5650
|
-
`export const REJECT_EXAMPLE_OF_${
|
|
5933
|
+
`export const REJECT_EXAMPLE_OF_${constantNameOf2(hole.id)} = ${JSON.stringify(hole.reject)} as const;`
|
|
5651
5934
|
);
|
|
5652
5935
|
}
|
|
5653
5936
|
}
|
|
@@ -5670,7 +5953,7 @@ function markdownGeneratedHeader(version) {
|
|
|
5670
5953
|
}
|
|
5671
5954
|
function carriersOf(hole, plan) {
|
|
5672
5955
|
return plan.operations.filter(
|
|
5673
|
-
(operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id) || operation.projectHoles.some((candidate) => candidate.id === hole.id) || operation.viewHoles.some((candidate) => candidate.id === hole.id)
|
|
5956
|
+
(operation) => operation.validateHoles.some((candidate) => candidate.id === hole.id) || operation.transformHole?.id === hole.id || operation.deriveHoles.some((candidate) => candidate.id === hole.id) || operation.projectHoles.some((candidate) => candidate.id === hole.id) || operation.viewHoles.some((candidate) => candidate.id === hole.id) || operation.aggregateHoles.some((candidate) => candidate.id === hole.id)
|
|
5674
5957
|
).map((operation) => `\`${operation.id}\` (${operation.method} ${operation.path})`);
|
|
5675
5958
|
}
|
|
5676
5959
|
function tick(name) {
|
|
@@ -5712,6 +5995,17 @@ function contractLines(hole, plan, shape) {
|
|
|
5712
5995
|
"## Champs",
|
|
5713
5996
|
...hole.writes.map((path) => `- ${tick(path)} : \xE9crit par le trou`)
|
|
5714
5997
|
];
|
|
5998
|
+
case "aggregate":
|
|
5999
|
+
return [
|
|
6000
|
+
`- lit : ${hole.reads.length === 0 ? "rien de l'entr\xE9e" : `${hole.reads.map(tick).join(", ")} de ${tick(hole.inputTypeName)}`}, les lignes de la ressource (${tick(`readonly ${hole.resourceTypeName}[]`)} : TOUTES celles du tenant, au sch\xE9ma, dans l'ordre de la lecture)${hole.from.length === 0 ? "" : ` et les lignes des tables partag\xE9es ${hole.from.map((table) => `${tick(table.sqlName)} (${tick(`shared.${table.tsName}: readonly ${table.typeName}[]`)})`).join(", ")}`}`,
|
|
6001
|
+
`- rend : ${tick(hole.writes)} de la sortie \u2014 ${tick(`${hole.outputTypeName}["${hole.writes}"]`)}, la VALEUR, jamais un ${tick("HoleResult")} : une agr\xE9gation ne refuse pas (D60)`,
|
|
6002
|
+
`- effets : ${effects(hole.hole.effects)}`,
|
|
6003
|
+
"- filtre : c'est le trou qui garde les lignes que l'entr\xE9e demande (une p\xE9riode, un site) ; le d\xE9p\xF4t ne filtre pas",
|
|
6004
|
+
"",
|
|
6005
|
+
"## Champs",
|
|
6006
|
+
...hole.reads.map((name, index) => `- ${tick(name)} : ${shape.reads[index] ?? "unknown"}`),
|
|
6007
|
+
`- ${tick(hole.writes)} (\xE9crit) : ${shape.returns}`
|
|
6008
|
+
];
|
|
5715
6009
|
case "view":
|
|
5716
6010
|
return [
|
|
5717
6011
|
`- corps : du SQL, dans la migration ${tick(plan.migrationPath)} entre ${tick(`-- @hole ${hole.id}`)} et ${tick(`-- @end-hole ${hole.id}`)} \u2014 ${tick(`create or replace view "${hole.name}" as \u2026`)}`,
|
|
@@ -5748,6 +6042,10 @@ function exampleLines(hole, plan) {
|
|
|
5748
6042
|
];
|
|
5749
6043
|
case "project":
|
|
5750
6044
|
return hole.accepts.map((accept) => `- contexte ${tick(JSON.stringify(accept.context))}, entr\xE9e ${tick(JSON.stringify(accept.input))} \u2192 ${tick(JSON.stringify(accept.output))}`);
|
|
6045
|
+
case "aggregate":
|
|
6046
|
+
return hole.accepts.map(
|
|
6047
|
+
(accept, index) => `- exemple ${index + 1} : entr\xE9e ${tick(JSON.stringify(accept.input))}, lignes ${tick(JSON.stringify(accept.rows))}${Object.keys(accept.shared).length === 0 ? "" : `, partag\xE9es ${tick(JSON.stringify(accept.shared))}`} \u2192 ${tick(hole.writes)} = ${tick(JSON.stringify(accept.value))} (les lignes sont compl\xE9t\xE9es sous leur sch\xE9ma par le test)`
|
|
6048
|
+
);
|
|
5751
6049
|
case "view":
|
|
5752
6050
|
return [
|
|
5753
6051
|
`- lignes de case d'UNE ligne de ressource : ${tick(JSON.stringify(hole.hole.examples.rows))}`,
|
|
@@ -5786,7 +6084,7 @@ function emitHoleDoc(plan, hole, library) {
|
|
|
5786
6084
|
"## Contrat",
|
|
5787
6085
|
`- appel\xE9 par : ${carriersOf(hole, plan).join(", ")}`,
|
|
5788
6086
|
...contractLines(hole, plan, shape),
|
|
5789
|
-
`- forme normalis\xE9e : ${shape.kind} [${(hole.kind === "transform" || hole.kind === "project" || hole.kind === "view" ? shape.columns : shape.reads).join(", ")}] \u2192 ${shape.returns}, code ${shape.codeRole} \u2014 shapeHash \`${shapeHash}\``,
|
|
6087
|
+
`- forme normalis\xE9e : ${shape.kind} [${(hole.kind === "transform" || hole.kind === "project" || hole.kind === "view" || hole.kind === "aggregate" ? shape.columns : shape.reads).join(", ")}] \u2192 ${shape.returns}, code ${shape.codeRole} \u2014 shapeHash \`${shapeHash}\``,
|
|
5790
6088
|
"",
|
|
5791
6089
|
hole.kind === "view" ? "## Colonnes (g\xE9n\xE9r\xE9es, le SQL doit les produire)" : "## Signature (g\xE9n\xE9r\xE9e)",
|
|
5792
6090
|
hole.kind === "view" ? "```sql" : "```ts",
|
|
@@ -5810,8 +6108,8 @@ function emitHoleDoc(plan, hole, library) {
|
|
|
5810
6108
|
"- aucune table hors de celles list\xE9es en `lit` ; aucune \xE9criture ; `null` quand rien n'est pos\xE9 (un crit\xE8re sans cible ne compte pas pour z\xE9ro)",
|
|
5811
6109
|
"- ne touche rien hors des marqueurs : le reste de la migration est r\xE9g\xE9n\xE9r\xE9"
|
|
5812
6110
|
] : [
|
|
5813
|
-
hole.kind === "project" ? "- pur : lit le contexte re\xE7u et la sortie re\xE7ue, rend une sortie du m\xEAme type ; jamais la base ni l'horloge" : "- pur hors de ses effets d\xE9clar\xE9s : ne lit ni horloge ni base ni r\xE9seau au-del\xE0 des param\xE8tres re\xE7us (AGENTS.md principe 5)",
|
|
5814
|
-
hole.kind === "project" ? "- rend la sortie, jamais une exception" : "- r\xE9pond par `accept(...)` ou `refuse(code, cause, suggestedFix[, line])` du runtime ; jamais par une exception",
|
|
6111
|
+
hole.kind === "project" ? "- pur : lit le contexte re\xE7u et la sortie re\xE7ue, rend une sortie du m\xEAme type ; jamais la base ni l'horloge" : hole.kind === "aggregate" ? "- pur hors de l'horloge quand elle est d\xE9clar\xE9e : lit l'entr\xE9e re\xE7ue, les lignes re\xE7ues et les lignes partag\xE9es re\xE7ues, rien d'autre (AGENTS.md principe 5) ; les dates sont des cha\xEEnes ISO, \xE0 comparer comme telles" : "- pur hors de ses effets d\xE9clar\xE9s : ne lit ni horloge ni base ni r\xE9seau au-del\xE0 des param\xE8tres re\xE7us (AGENTS.md principe 5)",
|
|
6112
|
+
hole.kind === "project" ? "- rend la sortie, jamais une exception" : hole.kind === "aggregate" ? "- rend la valeur, jamais une exception ; une agr\xE9gation sur z\xE9ro ligne rend la valeur neutre que la spec dit (0, [], null)" : "- r\xE9pond par `accept(...)` ou `refuse(code, cause, suggestedFix[, line])` du runtime ; jamais par une exception",
|
|
5815
6113
|
"- aucun `any`, aucune d\xE9pendance nouvelle, identifiants et commentaires en anglais",
|
|
5816
6114
|
"- ne touche rien hors du corps : signature, marqueurs et imports sont r\xE9g\xE9n\xE9r\xE9s"
|
|
5817
6115
|
]
|
|
@@ -5821,7 +6119,7 @@ function emitHoleDoc(plan, hole, library) {
|
|
|
5821
6119
|
|
|
5822
6120
|
// src/emit/tests.ts
|
|
5823
6121
|
var UUID_PATTERN = "/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u";
|
|
5824
|
-
function
|
|
6122
|
+
function constantNameOf3(typeName) {
|
|
5825
6123
|
return typeName.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").toUpperCase();
|
|
5826
6124
|
}
|
|
5827
6125
|
function invariant2(condition, message) {
|
|
@@ -5868,7 +6166,7 @@ function harnessLines(plan, options) {
|
|
|
5868
6166
|
const create = createOperation(plan);
|
|
5869
6167
|
const imports = [
|
|
5870
6168
|
`import { ${plan.tenant === null ? "afterEach, beforeEach" : "afterAll, beforeAll"}, describe, expect, it } from "vitest";`,
|
|
5871
|
-
...options.arranging ? ['import { fixedClock } from "@ogygie/core/clock";'] : [],
|
|
6169
|
+
...options.arranging && create !== null ? ['import { fixedClock } from "@ogygie/core/clock";'] : [],
|
|
5872
6170
|
`import type { ${plan.context.typeName} } from ${quote(plan.context.importPath)};`,
|
|
5873
6171
|
`import { ${plan.testBench.exportName} } from ${quote(plan.testBench.importPath)};`,
|
|
5874
6172
|
`import { ${[...fixtureNames].sort().join(", ")} } from ${quote(fixturesImportPath ?? "")};`
|
|
@@ -5936,6 +6234,33 @@ function harnessLines(plan, options) {
|
|
|
5936
6234
|
" return String(a) < String(b) ? -1 : 1;",
|
|
5937
6235
|
"}"
|
|
5938
6236
|
];
|
|
6237
|
+
const arrangingReport = options.arranging && options.report === true && create === null && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
|
|
6238
|
+
if (arrangingReport) {
|
|
6239
|
+
const handleType = plan.tenant === null ? `db: ${plan.databasePort?.typeName ?? "Database"}` : `context: ${plan.context.typeName}`;
|
|
6240
|
+
const handleName = plan.tenant === null ? "db" : "context";
|
|
6241
|
+
imports.push(
|
|
6242
|
+
`import type { ${plan.table.rowType} } from ${plan.target.database === "mssql-raw" ? '"./repository.gen.js"' : '"./db.schema.gen.js"'};`,
|
|
6243
|
+
`import { ${["arrange" + plan.resource.typeName + "Rows", ...plan.sharedTables.map((table) => `arrange${table.typeName}Rows`)].sort().join(", ")} } from "./repository.gen.js";`,
|
|
6244
|
+
`import type { ${[plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)].sort().join(", ")} } from "./schemas.js";`
|
|
6245
|
+
);
|
|
6246
|
+
if (plan.tenant === null && plan.databasePort !== null) imports.push(`import type { ${plan.databasePort.typeName} } from ${quote(plan.databasePort.importPath)};`);
|
|
6247
|
+
helpers.push(
|
|
6248
|
+
"",
|
|
6249
|
+
"/** Rows arranged at their schema (D60): the block has no create, the tests put their rows on the bench through the repository's arrangement. */",
|
|
6250
|
+
`async function arrangeRows(${handleType}, rows: readonly Omit<${plan.resource.typeName}, "id">[]): Promise<${plan.table.rowType}[]> {`,
|
|
6251
|
+
` return arrange${plan.resource.typeName}Rows(${handleName}, rows);`,
|
|
6252
|
+
"}"
|
|
6253
|
+
);
|
|
6254
|
+
if (plan.sharedTables.length > 0) {
|
|
6255
|
+
helpers.push(
|
|
6256
|
+
"",
|
|
6257
|
+
"/** The shared rows of an aggregate example (D60), arranged in their tables before the request. */",
|
|
6258
|
+
`async function arrangeShared(${handleType}, shared: { ${plan.sharedTables.map((table) => `${table.tsName}?: readonly ${table.typeName}[]`).join("; ")} }): Promise<void> {`,
|
|
6259
|
+
...plan.sharedTables.map((table) => ` if (shared.${table.tsName} !== undefined) await arrange${table.typeName}Rows(${handleName}, shared.${table.tsName});`),
|
|
6260
|
+
"}"
|
|
6261
|
+
);
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
5939
6264
|
if (options.arranging && create !== null) {
|
|
5940
6265
|
imports.push(
|
|
5941
6266
|
plan.target.database === "mssql-raw" ? `import { insert${plan.resource.typeName}, type ${plan.table.rowType} } from "./repository.gen.js";` : `import type { ${plan.table.rowType} } from "./db.schema.gen.js";`,
|
|
@@ -6034,7 +6359,7 @@ function emitContractTests(plan) {
|
|
|
6034
6359
|
);
|
|
6035
6360
|
}
|
|
6036
6361
|
const invalid = operation.kind === "delete" ? { ok: false, reason: "no body" } : invalidSampleOf(operation.inputModel);
|
|
6037
|
-
const invalidConstant = `INVALID_${
|
|
6362
|
+
const invalidConstant = `INVALID_${constantNameOf3(operation.inputType)}`;
|
|
6038
6363
|
const hasInvalid = invalid.ok || operation.kind === "update";
|
|
6039
6364
|
if (hasInvalid) {
|
|
6040
6365
|
factoryImports.add(invalidConstant);
|
|
@@ -6148,7 +6473,14 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6148
6473
|
referenced: false,
|
|
6149
6474
|
key: "ARRANGED_KEY",
|
|
6150
6475
|
snapshotBefore: behavior.expectations.some((expectation) => expectation.raw === "rien n'est \xE9crit"),
|
|
6151
|
-
needsReplacement: false
|
|
6476
|
+
needsReplacement: false,
|
|
6477
|
+
sharedExample: null,
|
|
6478
|
+
bindArranged: !operation.report
|
|
6479
|
+
};
|
|
6480
|
+
const examplesOf = (aggregate) => {
|
|
6481
|
+
const constant = `EXAMPLES_OF_${constantNameOf(aggregate.id)}`;
|
|
6482
|
+
useFactory(constant);
|
|
6483
|
+
return constant;
|
|
6152
6484
|
};
|
|
6153
6485
|
const useFactory = (name) => {
|
|
6154
6486
|
factoryImports.add(name);
|
|
@@ -6175,11 +6507,21 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6175
6507
|
if (key === `with:${guard.entry.valid.case}`) continue;
|
|
6176
6508
|
if (key === "with:entr\xE9e valide") continue;
|
|
6177
6509
|
if (key === "with:entr\xE9e invalide") {
|
|
6178
|
-
const constant = `INVALID_${
|
|
6510
|
+
const constant = `INVALID_${constantNameOf3(operation.inputType)}`;
|
|
6179
6511
|
useFactory(constant);
|
|
6180
6512
|
derivation.input = constant;
|
|
6181
6513
|
continue;
|
|
6182
6514
|
}
|
|
6515
|
+
const firstAggregate = operation.aggregateHoles[0] ?? null;
|
|
6516
|
+
if (operation.kind === "read" && operation.motif === null && createFactory === null && firstAggregate !== null) {
|
|
6517
|
+
if (key === "with:lignes") {
|
|
6518
|
+
const constant = examplesOf(firstAggregate);
|
|
6519
|
+
derivation.arrangements.push(`...(${constant}[0]?.rows ?? [])`);
|
|
6520
|
+
if (firstAggregate.from.length > 0) derivation.sharedExample = `${constant}[0]?.shared ?? {}`;
|
|
6521
|
+
continue;
|
|
6522
|
+
}
|
|
6523
|
+
if (key === "without:ligne") continue;
|
|
6524
|
+
}
|
|
6183
6525
|
if (operation.kind === "read" && operation.motif === null && createFactory !== null) {
|
|
6184
6526
|
if (key === "with:lignes") {
|
|
6185
6527
|
derivation.arrangements.push(`${createFactory}(0)`, `${createFactory}(1)`);
|
|
@@ -6294,7 +6636,7 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6294
6636
|
const refusedByHole = hole !== null && behavior.code !== null && ((hole.kind === "validate" || hole.kind === "transform" || hole.kind === "derive") && hole.errorCode === behavior.code);
|
|
6295
6637
|
if (hole !== null && refusedByHole) {
|
|
6296
6638
|
if ((hole.kind === "validate" || hole.kind === "derive" && hole.reject !== null) && inputFactory !== null) {
|
|
6297
|
-
const constant = `REJECT_EXAMPLE_OF_${
|
|
6639
|
+
const constant = `REJECT_EXAMPLE_OF_${constantNameOf3(hole.id)}`;
|
|
6298
6640
|
useFactory(constant);
|
|
6299
6641
|
derivation.overrides.push(`...${constant}`);
|
|
6300
6642
|
} else if (hole.kind === "transform") {
|
|
@@ -6314,6 +6656,16 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6314
6656
|
derivation.needsReplacement = true;
|
|
6315
6657
|
derivation.expectedIds = "[arranged[0]?.id, replacement.id]";
|
|
6316
6658
|
}
|
|
6659
|
+
if (hole !== null && hole.kind === "aggregate") {
|
|
6660
|
+
const constant = examplesOf(hole);
|
|
6661
|
+
derivation.arrangements = [`...(${constant}[0]?.rows ?? [])`];
|
|
6662
|
+
derivation.sharedExample = hole.from.length === 0 ? null : `${constant}[0]?.shared ?? {}`;
|
|
6663
|
+
if (inputFactory === null) derivation.input = `(${constant}[0]?.input ?? {})`;
|
|
6664
|
+
else {
|
|
6665
|
+
useFactory(inputFactory);
|
|
6666
|
+
derivation.input = `{ ...${inputFactory}(), ...(${constant}[0]?.input ?? {}) }`;
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6317
6669
|
if (createFactory !== null && derivation.arrangements.length > 0) useFactory(createFactory);
|
|
6318
6670
|
if (inputFactory !== null && derivation.input !== null && derivation.input.startsWith(inputFactory)) useFactory(inputFactory);
|
|
6319
6671
|
if (inputFactory !== null && (derivation.overrides.length > 0 || derivation.queryOverrides.length > 0)) {
|
|
@@ -6330,8 +6682,9 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6330
6682
|
}
|
|
6331
6683
|
const handle = arrangingHandle(plan, "member");
|
|
6332
6684
|
if (derivation.arrangements.length > 0) {
|
|
6333
|
-
lines.push(` const arranged = await arrangeRows(${handle}, [${derivation.arrangements.join(", ")}]);`);
|
|
6685
|
+
lines.push(` ${derivation.bindArranged ? "const arranged = " : ""}await arrangeRows(${handle}, [${derivation.arrangements.join(", ")}]);`);
|
|
6334
6686
|
}
|
|
6687
|
+
if (derivation.sharedExample !== null) lines.push(` await arrangeShared(${handle}, ${derivation.sharedExample});`);
|
|
6335
6688
|
if (derivation.needsReplacement && createFactory !== null) {
|
|
6336
6689
|
lines.push(` const replacement = await insert${plan.resource.typeName}(${handle}, ${createFactory}(0, { supersedes: arranged[0]?.id })${plan.signed === null ? "" : ", ARRANGED_SIGNATURE"}, CLOCK);`);
|
|
6337
6690
|
}
|
|
@@ -6442,6 +6795,9 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6442
6795
|
const target = signatureTarget();
|
|
6443
6796
|
const expected = value === "renouvel\xE9e" ? `${plan.signature?.exportName ?? "signerOf"}(${derivation.session}.context)` : "ARRANGED_SIGNATURE.by";
|
|
6444
6797
|
lines.push(` expect(${target}?.${plan.signed.by}).toBe(${expected});`);
|
|
6798
|
+
} else if (value.endsWith(" d\xE9riv\xE9") && operation.aggregateHoles.some((candidate) => value === `${candidate.writes} d\xE9riv\xE9`)) {
|
|
6799
|
+
const writer = operation.aggregateHoles.find((candidate) => value === `${candidate.writes} d\xE9riv\xE9`);
|
|
6800
|
+
if (writer !== void 0) lines.push(` expect(output.${subPath}).toEqual(${examplesOf(writer)}[0]?.value);`);
|
|
6445
6801
|
} else if (value.endsWith(" d\xE9riv\xE9") && operation.deriveHoles.some((candidate) => value === `${candidate.writes} d\xE9riv\xE9`)) {
|
|
6446
6802
|
const field = value.slice(0, -" d\xE9riv\xE9".length);
|
|
6447
6803
|
lines.push(` expect(valuesAt(${root}, ${quote(subPath)})).toEqual([DERIVED_OF_VALID_CREATES[0]?.[${quote(field)}]]);`);
|
|
@@ -6502,7 +6858,7 @@ function behaviorTestLines(plan, item, factoryImports, schemaImports, repository
|
|
|
6502
6858
|
return lines;
|
|
6503
6859
|
}
|
|
6504
6860
|
function emitBehaviorTests(plan) {
|
|
6505
|
-
const { imports, helpers } = harnessLines(plan, { arranging: true });
|
|
6861
|
+
const { imports, helpers } = harnessLines(plan, { arranging: true, report: true });
|
|
6506
6862
|
const factoryImports = /* @__PURE__ */ new Set();
|
|
6507
6863
|
const schemaImports = /* @__PURE__ */ new Set();
|
|
6508
6864
|
const repositoryImports = /* @__PURE__ */ new Set();
|
|
@@ -7034,6 +7390,39 @@ function referenceLines2(plan, reference) {
|
|
|
7034
7390
|
"}"
|
|
7035
7391
|
];
|
|
7036
7392
|
}
|
|
7393
|
+
function sharedReadLines2(plan, table) {
|
|
7394
|
+
return [
|
|
7395
|
+
`/** The rows of the shared table ${table.sqlName} (D60) for the tenant, through its read-only mapping \u2014 what the aggregate holes receive, at their schema. */`,
|
|
7396
|
+
`export async function select${table.typeName}Rows(ctx: ${plan.context.typeName}): Promise<${table.rowType}[]> {`,
|
|
7397
|
+
` return tenantDb(ctx).select(${table.tsName}).all();`,
|
|
7398
|
+
"}"
|
|
7399
|
+
];
|
|
7400
|
+
}
|
|
7401
|
+
function insertExpressionOf(field) {
|
|
7402
|
+
if (field.model.kind === "string" && field.model.format === "datetime") {
|
|
7403
|
+
return field.model.nullable ? `row.${field.name} === null ? null : new Date(row.${field.name})` : `new Date(row.${field.name})`;
|
|
7404
|
+
}
|
|
7405
|
+
return `row.${field.name}`;
|
|
7406
|
+
}
|
|
7407
|
+
function arrangeLines2(plan, target) {
|
|
7408
|
+
const valuesType = target.newType === null ? `typeof ${target.tsName}.$inferInsert` : target.newType;
|
|
7409
|
+
return [
|
|
7410
|
+
`/** TESTS ONLY (D60): rows of ${target.sqlTable} arranged at their schema \u2014 the feature never writes them, they come from elsewhere; the tests of a block without create put their rows on the bench here. */`,
|
|
7411
|
+
`export async function arrange${target.typeName}Rows(ctx: ${plan.context.typeName}, rows: readonly ${target.withId ? `Omit<${target.typeName}, "id">` : target.typeName}[]): Promise<${target.rowType}[]> {`,
|
|
7412
|
+
` const inserted: ${target.rowType}[] = [];`,
|
|
7413
|
+
" for (const row of rows) {",
|
|
7414
|
+
` const values: Omit<${valuesType}, ${quote(tenantOf(plan).columnKey)}> = {`,
|
|
7415
|
+
...target.withId ? [" id: crypto.randomUUID(),"] : [],
|
|
7416
|
+
...target.fields.filter((field) => !(target.withId && field.name === "id")).map((field) => ` ${field.name}: ${insertExpressionOf(field)},`),
|
|
7417
|
+
" };",
|
|
7418
|
+
` const [written] = await tenantDb(ctx).insert(${target.tsName}).values(values).returning();`,
|
|
7419
|
+
" if (written === undefined) throwOgygieError(insertReturnedNothing());",
|
|
7420
|
+
" inserted.push(written);",
|
|
7421
|
+
" }",
|
|
7422
|
+
" return inserted;",
|
|
7423
|
+
"}"
|
|
7424
|
+
];
|
|
7425
|
+
}
|
|
7037
7426
|
function viewReadLines2(plan, view) {
|
|
7038
7427
|
return [
|
|
7039
7428
|
`/** The rows of the view ${view.name} (hole ${view.id}, D32) for the tenant, through its read-only mapping. */`,
|
|
@@ -7064,16 +7453,23 @@ function emitRepository(plan) {
|
|
|
7064
7453
|
}
|
|
7065
7454
|
const queryTypes = reads.filter((operation) => operation.motif?.method === "currentByKey").map((operation) => operation.inputType);
|
|
7066
7455
|
const inputTypes = [...new Set([create?.inputType, update?.inputType, plan.importMotif?.rowType, ...queryTypes].filter((name) => name !== void 0))].sort();
|
|
7067
|
-
const
|
|
7456
|
+
const arranging = create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0);
|
|
7457
|
+
const resourceTypes = [...new Set([plan.key === null ? null : plan.resource.typeName, ...arranging ? [plan.resource.typeName, ...plan.sharedTables.map((table) => table.typeName)] : [], ...setOrClear?.slot === null || setOrClear?.slot === void 0 ? [] : [setOrClear.slot.typeName]].filter((name) => name !== null))].sort();
|
|
7068
7458
|
const tableImports = [`${plan.table.tsName}, type ${plan.table.newType}, type ${plan.table.rowType}`];
|
|
7069
7459
|
for (const slot of plan.slots) tableImports.push(`${slot.table.tsName}, type ${slot.table.newType}, type ${slot.table.rowType}`);
|
|
7070
7460
|
for (const reference of plan.references) if (reference.table !== null) tableImports.push(`${reference.table.tsName}, type ${reference.table.newType}, type ${reference.table.rowType}`);
|
|
7071
7461
|
for (const view of views) tableImports.push(`${view.tsName}, type ${view.rowType}`);
|
|
7462
|
+
for (const table of plan.sharedTables) tableImports.push(`${table.tsName}, type ${table.rowType}`);
|
|
7072
7463
|
const sections = [];
|
|
7073
7464
|
for (const operation of reads) sections.push("", ...readFunctionLines2(plan, operation));
|
|
7074
7465
|
for (const slot of plan.slots) sections.push("", ...slotReadLines2(plan, slot));
|
|
7075
7466
|
for (const reference of plan.references) sections.push("", ...referenceLines2(plan, reference));
|
|
7076
7467
|
for (const view of views) sections.push("", ...viewReadLines2(plan, view));
|
|
7468
|
+
for (const table of plan.sharedTables) sections.push("", ...sharedReadLines2(plan, table));
|
|
7469
|
+
if (create === void 0 && plan.operations.some((operation) => operation.aggregateHoles.length > 0)) {
|
|
7470
|
+
sections.push("", ...arrangeLines2(plan, { sqlTable: plan.table.sqlName, tsName: plan.table.tsName, rowType: plan.table.rowType, newType: plan.table.newType, typeName: plan.resource.typeName, fields: plan.resource.fields, withId: true }));
|
|
7471
|
+
for (const table of plan.sharedTables) sections.push("", ...arrangeLines2(plan, { sqlTable: table.sqlName, tsName: table.tsName, rowType: table.rowType, newType: null, typeName: table.typeName, fields: table.fields, withId: false }));
|
|
7472
|
+
}
|
|
7077
7473
|
if (plan.key !== null) sections.push("", ...loadByKeyLines2(plan));
|
|
7078
7474
|
if (create !== void 0) sections.push("", ...insertFunctionLines2(plan, create));
|
|
7079
7475
|
if (update !== void 0) sections.push("", ...updateFunctionLines2(plan, update));
|
|
@@ -7340,7 +7736,51 @@ function emitService(plan) {
|
|
|
7340
7736
|
const signatureLine = plan.signed === null ? [] : [` const signature: WriteSignature = { by: ${plan.signature?.exportName ?? "signerOf"}(deps.ctx), at: deps.clock.now() };`];
|
|
7341
7737
|
const signatureArgument = plan.signed === null ? "" : ", signature";
|
|
7342
7738
|
if (plan.signed !== null) repositoryImports.add("type WriteSignature");
|
|
7739
|
+
const projectionLines = (operation) => {
|
|
7740
|
+
if (operation.projectHoles.length === 0) return [" return computed;"];
|
|
7741
|
+
const lines = [];
|
|
7742
|
+
if (plan.tenant === null) lines.push(" const projection = deps.ctx;");
|
|
7743
|
+
else lines.push(" const { db: _db, ...projection } = deps.ctx;", " void _db;");
|
|
7744
|
+
let current = "computed";
|
|
7745
|
+
for (const hole of operation.projectHoles) {
|
|
7746
|
+
lines.push(` const ${hole.id}Output = ${hole.id}(projection, ${current});`);
|
|
7747
|
+
current = `${hole.id}Output`;
|
|
7748
|
+
}
|
|
7749
|
+
lines.push(` return ${current};`);
|
|
7750
|
+
return lines;
|
|
7751
|
+
};
|
|
7752
|
+
const reportBodyLines = (operation) => {
|
|
7753
|
+
const selectName = `select${operation.pascalId}`;
|
|
7754
|
+
repositoryImports.add(selectName);
|
|
7755
|
+
const lines = [` const rows = (await ${selectName}(${handle})).map(${dto});`];
|
|
7756
|
+
const sharedUsed = /* @__PURE__ */ new Map();
|
|
7757
|
+
for (const hole of operation.aggregateHoles) for (const table of hole.from) sharedUsed.set(table.sqlName, table);
|
|
7758
|
+
for (const table of sharedUsed.values()) {
|
|
7759
|
+
repositoryImports.add(`select${table.typeName}Rows`);
|
|
7760
|
+
dbImports.add(`type ${table.rowType}`);
|
|
7761
|
+
typeImports.add(table.typeName);
|
|
7762
|
+
lines.push(` const ${table.tsName}Rows = (await select${table.typeName}Rows(${handle})).map(to${table.typeName}Dto);`);
|
|
7763
|
+
}
|
|
7764
|
+
for (const hole of operation.aggregateHoles) {
|
|
7765
|
+
const picked = hole.reads.map((field) => `${field}: input.${field}`).join(", ");
|
|
7766
|
+
const shared = hole.from.length === 0 ? "" : `, { ${hole.from.map((table) => `${table.tsName}: ${table.tsName}Rows`).join(", ")} }`;
|
|
7767
|
+
const call = `${hole.id}({ ${picked} }, rows${shared}${effectArguments(plan, hole)})`;
|
|
7768
|
+
lines.push(` const ${hole.id}Value = ${isAsyncHole(hole) ? `await ${call}` : call};`);
|
|
7769
|
+
}
|
|
7770
|
+
const rootFields = [];
|
|
7771
|
+
if (operation.outputModel.kind === "object") {
|
|
7772
|
+
for (const [name, model] of Object.entries(operation.outputModel.fields)) {
|
|
7773
|
+
const hole = operation.aggregateHoles.find((candidate) => candidate.writes === name);
|
|
7774
|
+
if (hole !== void 0) rootFields.push(` ${name}: ${hole.id}Value,`);
|
|
7775
|
+
else if (operation.echoFields.includes(name)) rootFields.push(` ${name}: input.${name},`);
|
|
7776
|
+
else rootFields.push(` ${name}: ${neutralValueOf(model)},`);
|
|
7777
|
+
}
|
|
7778
|
+
}
|
|
7779
|
+
lines.push(` const computed: ${operation.outputType} = {`, ...rootFields, " };", ...projectionLines(operation));
|
|
7780
|
+
return lines;
|
|
7781
|
+
};
|
|
7343
7782
|
const readBodyLines = (operation) => {
|
|
7783
|
+
if (operation.report) return reportBodyLines(operation);
|
|
7344
7784
|
const element = operation.element;
|
|
7345
7785
|
if (element === null) {
|
|
7346
7786
|
const selectName2 = `select${operation.pascalId}`;
|
|
@@ -7400,20 +7840,9 @@ function emitService(plan) {
|
|
|
7400
7840
|
...elementFields,
|
|
7401
7841
|
" })),",
|
|
7402
7842
|
...rootFields,
|
|
7403
|
-
" };"
|
|
7843
|
+
" };",
|
|
7844
|
+
...projectionLines(operation)
|
|
7404
7845
|
);
|
|
7405
|
-
if (operation.projectHoles.length === 0) {
|
|
7406
|
-
lines.push(" return computed;");
|
|
7407
|
-
} else {
|
|
7408
|
-
if (plan.tenant === null) lines.push(" const projection = deps.ctx;");
|
|
7409
|
-
else lines.push(" const { db: _db, ...projection } = deps.ctx;", " void _db;");
|
|
7410
|
-
let current = "computed";
|
|
7411
|
-
for (const hole of operation.projectHoles) {
|
|
7412
|
-
lines.push(` const ${hole.id}Output = ${hole.id}(projection, ${current});`);
|
|
7413
|
-
current = `${hole.id}Output`;
|
|
7414
|
-
}
|
|
7415
|
-
lines.push(` return ${current};`);
|
|
7416
|
-
}
|
|
7417
7846
|
return lines;
|
|
7418
7847
|
};
|
|
7419
7848
|
const respondLines = (operation) => {
|
|
@@ -7429,7 +7858,7 @@ function emitService(plan) {
|
|
|
7429
7858
|
"",
|
|
7430
7859
|
`/** ${operation.method} ${operation.path} \u2014 ${operation.id}. */`,
|
|
7431
7860
|
`export async function ${operation.serviceName}(deps: ${deps}, input: ${operation.inputType}): Promise<${operation.outputType}> {`,
|
|
7432
|
-
...operation.echoFields.length === 0 && operation.motif?.method !== "currentByKey" ? [" void input;"] : [],
|
|
7861
|
+
...operation.echoFields.length === 0 && operation.motif?.method !== "currentByKey" && !operation.aggregateHoles.some((hole) => hole.reads.length > 0) ? [" void input;"] : [],
|
|
7433
7862
|
...readBodyLines(operation),
|
|
7434
7863
|
"}"
|
|
7435
7864
|
);
|
|
@@ -7588,6 +8017,16 @@ function emitService(plan) {
|
|
|
7588
8017
|
" };",
|
|
7589
8018
|
"}"
|
|
7590
8019
|
];
|
|
8020
|
+
const sharedUsedByHoles = plan.sharedTables.filter((table) => plan.holes.some((hole) => hole.kind === "aggregate" && hole.from.some((candidate) => candidate.sqlName === table.sqlName)));
|
|
8021
|
+
const sharedDtoLines = sharedUsedByHoles.flatMap((table) => [
|
|
8022
|
+
"",
|
|
8023
|
+
`/** One row of the shared table ${table.sqlName} at its schema (D60): what the aggregate holes receive. */`,
|
|
8024
|
+
`function to${table.typeName}Dto(row: ${table.rowType}): ${table.typeName} {`,
|
|
8025
|
+
" return {",
|
|
8026
|
+
...table.fields.map((field) => ` ${field.name}: ${field.dtoExpression},`),
|
|
8027
|
+
" };",
|
|
8028
|
+
"}"
|
|
8029
|
+
]);
|
|
7591
8030
|
const slotHelpers = plan.slots.flatMap((slot) => {
|
|
7592
8031
|
const dtoFields = slot.fields.map((field) => `${field.name}: ${field.dtoExpression}`).join(", ");
|
|
7593
8032
|
const keyValues = slot.keyValues === null ? null : `KEY_VALUES_OF_${slot.name.toUpperCase()}`;
|
|
@@ -7636,6 +8075,7 @@ ${[...typeImports].sort().map((name) => ` type ${name},`).join("\n")}
|
|
|
7636
8075
|
"}",
|
|
7637
8076
|
"",
|
|
7638
8077
|
...dtoLines,
|
|
8078
|
+
...sharedDtoLines,
|
|
7639
8079
|
...slotHelpers,
|
|
7640
8080
|
"",
|
|
7641
8081
|
"/** A hole's refusal becomes the operation's 400 OgygieError. */",
|
|
@@ -7758,7 +8198,8 @@ function manifestEntryOf(plan) {
|
|
|
7758
8198
|
})),
|
|
7759
8199
|
// The views of the view holes are read through Drizzle like tables: what the repository reads is on the map (D32).
|
|
7760
8200
|
// D33 precision (M3): the manifest gate proves a declared table by its pgTable — a SQL Server product has none, so its entry declares no table; the generated block of its spec is the map.
|
|
7761
|
-
|
|
8201
|
+
// D60: a shared table read through its Drizzle mapping is on the map like a view — the manifest gate asks every pgTable of the folder to be declared.
|
|
8202
|
+
dbTables: plan.target.database === "mssql-raw" ? [] : [...tablesOf(plan).map((table) => table.sqlName), ...plan.holes.flatMap((hole) => hole.kind === "view" ? [hole.name] : []), ...plan.sharedTables.map((table) => table.sqlName)]
|
|
7762
8203
|
};
|
|
7763
8204
|
}
|
|
7764
8205
|
function canonicalJson(value) {
|
|
@@ -7990,6 +8431,8 @@ function holeLine(hole, plan) {
|
|
|
7990
8431
|
return `- ${tick3(hole.id)} \u2014 ${tick3("validate")}, lit ${hole.reads.map(tick3).join(", ")} de ${tick3(hole.inputTypeName)}, refuse en ${tick3(hole.errorCode)} (400) ; appel\xE9 par ${carriers} ; ${context}`;
|
|
7991
8432
|
case "derive":
|
|
7992
8433
|
return `- ${tick3(hole.id)} \u2014 ${tick3("derive")}, lit ${hole.reads.map(tick3).join(", ")} de ${tick3(hole.inputTypeName)}, \xE9crit ${tick3(hole.writes)}${hole.errorCode === null ? "" : `, refuse en ${tick3(hole.errorCode)} (400)`} ; appel\xE9 par ${carriers} ; ${context}`;
|
|
8434
|
+
case "aggregate":
|
|
8435
|
+
return `- ${tick3(hole.id)} \u2014 ${tick3("aggregate")} (D60), lit ${hole.reads.length === 0 ? "rien de l'entr\xE9e" : `${hole.reads.map(tick3).join(", ")} de ${tick3(hole.inputTypeName)}`}, les lignes de ${tick3(hole.resourceTypeName)}${hole.from.length === 0 ? "" : ` et ${hole.from.map((table) => tick3(table.sqlName)).join(", ")}`}, \xE9crit ${tick3(hole.writes)} de la sortie ; appel\xE9 par ${carriers} ; ${context}`;
|
|
7993
8436
|
case "project":
|
|
7994
8437
|
return `- ${tick3(hole.id)} \u2014 ${tick3("project")}, lit ${hole.reads.map(tick3).join(", ")} du contexte de l'appelant, \xE9crit ${hole.writes.map(tick3).join(", ")} ; appel\xE9 par ${carriers} ; ${context}`;
|
|
7995
8438
|
case "view":
|
|
@@ -8037,6 +8480,7 @@ function emitSpecContracts(plan) {
|
|
|
8037
8480
|
...tableSection(plan.table, plan, indexes.length === 0 ? [] : [`index ${indexes.join(" ; ")}`]),
|
|
8038
8481
|
...detailTables.flatMap((table) => ["", ...tableSection(table, plan, [])]),
|
|
8039
8482
|
...views.flatMap((view) => ["", ...viewSection(view)]),
|
|
8483
|
+
...plan.sharedTables.flatMap((table) => ["", `${tick3(table.sqlName)} \u2014 table PARTAG\xC9E (D60) : table du produit hors du bloc, lue par les trous aggregate qui la nomment, typ\xE9e par ${tick3(table.schemaName)}, jamais cr\xE9\xE9e ni \xE9crite ici ; mapping en lecture seule dans ${tick3("db.schema.gen.ts")}.`]),
|
|
8040
8484
|
"",
|
|
8041
8485
|
"## Trous (D23)",
|
|
8042
8486
|
"",
|
|
@@ -11504,10 +11948,14 @@ export {
|
|
|
11504
11948
|
tableNameOf,
|
|
11505
11949
|
tableRefOf,
|
|
11506
11950
|
parentKeyFieldOf,
|
|
11951
|
+
SharedTableSchema,
|
|
11952
|
+
sharedTableNameOf,
|
|
11953
|
+
typedSharedTablesOf,
|
|
11507
11954
|
OperationSchema,
|
|
11508
11955
|
isHttpOperation,
|
|
11509
11956
|
isWriteOperation,
|
|
11510
11957
|
isKeyedOperation,
|
|
11958
|
+
AggregateHoleSchema,
|
|
11511
11959
|
HoleSchema,
|
|
11512
11960
|
acceptExamplesOf,
|
|
11513
11961
|
HOLE_KIND_SERVES,
|
|
@@ -11588,6 +12036,7 @@ export {
|
|
|
11588
12036
|
tablesOf,
|
|
11589
12037
|
ownedTables,
|
|
11590
12038
|
viewBodiesOf,
|
|
12039
|
+
deepOverlay,
|
|
11591
12040
|
normalizedTypeOf,
|
|
11592
12041
|
HoleShapeSchema,
|
|
11593
12042
|
HoleProvenanceSchema,
|
|
@@ -11603,7 +12052,6 @@ export {
|
|
|
11603
12052
|
HOLES_LIBRARY_DIR,
|
|
11604
12053
|
loadHolesLibrary,
|
|
11605
12054
|
libraryFileNameOf,
|
|
11606
|
-
deepOverlay,
|
|
11607
12055
|
GENERATED_FEATURE_FILES,
|
|
11608
12056
|
generateFeature,
|
|
11609
12057
|
migrationFileNameFor,
|