appflare 0.2.15 → 0.2.17
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/Documentation.md +27 -0
- package/cli/schema-compiler.ts +432 -2
- package/cli/templates/dashboard/builders/table-routes/fragments.ts +8 -5
- package/cli/templates/dashboard/builders/table-routes/table/index.ts +1 -1
- package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +3 -2
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +98 -7
- package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +4 -2
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +360 -11
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +170 -25
- package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +14 -4
- package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +52 -16
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +546 -7
- package/dist/cli/index.js +1609 -393
- package/dist/cli/index.mjs +1609 -393
- package/dist/index.d.mts +27 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/schema.ts +57 -1
- package/dist/cli/index.d.mts +0 -2
- package/dist/cli/index.d.ts +0 -2
package/Documentation.md
CHANGED
|
@@ -45,6 +45,33 @@ Schema is defined with `schema`, `table`, and `v` from Appflare.
|
|
|
45
45
|
|
|
46
46
|
Example source: `packages/backend/schema.ts`.
|
|
47
47
|
|
|
48
|
+
### 2.3 Relation helpers (`v.one`, `v.many`, `v.manyToMany`)
|
|
49
|
+
|
|
50
|
+
- `v.one("target")` creates a single-reference relation and infers a local FK field.
|
|
51
|
+
- `v.many("target")` is inverse one-to-many and infers an FK on the target table.
|
|
52
|
+
- `v.manyToMany("target")` creates a many-to-many relation by synthesizing a junction table.
|
|
53
|
+
|
|
54
|
+
Many-to-many example:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export const schemas = schema({
|
|
58
|
+
pets: table({
|
|
59
|
+
id: v.uuid(),
|
|
60
|
+
trips: v.manyToMany("trips"),
|
|
61
|
+
}),
|
|
62
|
+
trips: table({
|
|
63
|
+
id: v.uuid(),
|
|
64
|
+
pets: v.manyToMany("pets"),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Default behavior for `v.manyToMany`:
|
|
70
|
+
|
|
71
|
+
- Generates one deterministic junction table per pair.
|
|
72
|
+
- Junction rows are pure links (two FK columns, no payload columns).
|
|
73
|
+
- Reciprocal declarations must agree on options (`junctionTable`, field names, FK actions), otherwise generation throws a conflict error.
|
|
74
|
+
|
|
48
75
|
---
|
|
49
76
|
|
|
50
77
|
## 3) How to create handlers
|
package/cli/schema-compiler.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ColumnDefinition,
|
|
6
6
|
ColumnType,
|
|
7
7
|
ManyRelationDefinition,
|
|
8
|
+
ManyToManyRelationDefinition,
|
|
8
9
|
OneRelationDefinition,
|
|
9
10
|
SchemaDefinition,
|
|
10
11
|
TableDefinition,
|
|
@@ -196,6 +197,296 @@ function resolveNotNullFromNullableFlags(
|
|
|
196
197
|
return defaultNotNull;
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
type ManyToManyPair = {
|
|
201
|
+
leftTable: string;
|
|
202
|
+
leftReferenceField: string;
|
|
203
|
+
rightTable: string;
|
|
204
|
+
rightReferenceField: string;
|
|
205
|
+
junctionTable: string;
|
|
206
|
+
leftField: string;
|
|
207
|
+
rightField: string;
|
|
208
|
+
leftSqlName?: string;
|
|
209
|
+
rightSqlName?: string;
|
|
210
|
+
onDelete?: OneRelationDefinition["onDelete"];
|
|
211
|
+
onUpdate?: OneRelationDefinition["onUpdate"];
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
function endpointKey(tableName: string, referenceField: string): string {
|
|
215
|
+
return `${tableName}:${referenceField}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeManyToManyPairKey(
|
|
219
|
+
sourceTable: string,
|
|
220
|
+
sourceReferenceField: string,
|
|
221
|
+
targetTable: string,
|
|
222
|
+
targetReferenceField: string,
|
|
223
|
+
): {
|
|
224
|
+
key: string;
|
|
225
|
+
leftTable: string;
|
|
226
|
+
leftReferenceField: string;
|
|
227
|
+
rightTable: string;
|
|
228
|
+
rightReferenceField: string;
|
|
229
|
+
sourceIsLeft: boolean;
|
|
230
|
+
} {
|
|
231
|
+
const source = endpointKey(sourceTable, sourceReferenceField);
|
|
232
|
+
const target = endpointKey(targetTable, targetReferenceField);
|
|
233
|
+
const sourceIsLeft = source <= target;
|
|
234
|
+
|
|
235
|
+
if (sourceIsLeft) {
|
|
236
|
+
return {
|
|
237
|
+
key: `${source}|${target}`,
|
|
238
|
+
leftTable: sourceTable,
|
|
239
|
+
leftReferenceField: sourceReferenceField,
|
|
240
|
+
rightTable: targetTable,
|
|
241
|
+
rightReferenceField: targetReferenceField,
|
|
242
|
+
sourceIsLeft: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
key: `${target}|${source}`,
|
|
248
|
+
leftTable: targetTable,
|
|
249
|
+
leftReferenceField: targetReferenceField,
|
|
250
|
+
rightTable: sourceTable,
|
|
251
|
+
rightReferenceField: sourceReferenceField,
|
|
252
|
+
sourceIsLeft: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function defaultManyToManyJunctionTable(
|
|
257
|
+
leftTable: string,
|
|
258
|
+
rightTable: string,
|
|
259
|
+
): string {
|
|
260
|
+
return `${leftTable}${toPascalCase(rightTable)}Links`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function defaultManyToManyFieldName(
|
|
264
|
+
ownerTable: string,
|
|
265
|
+
otherTable: string,
|
|
266
|
+
suffix: "source" | "target",
|
|
267
|
+
): string {
|
|
268
|
+
const ownerSingular = singularize(ownerTable);
|
|
269
|
+
const otherSingular = singularize(otherTable);
|
|
270
|
+
|
|
271
|
+
if (ownerSingular === otherSingular) {
|
|
272
|
+
return `${suffix}${toPascalCase(ownerSingular)}Id`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return `${ownerSingular}Id`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function mergeManyToManyPairConfig(
|
|
279
|
+
existing: ManyToManyPair,
|
|
280
|
+
incoming: ManyToManyPair,
|
|
281
|
+
pairKey: string,
|
|
282
|
+
): ManyToManyPair {
|
|
283
|
+
if (existing.junctionTable !== incoming.junctionTable) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`manyToMany pair '${pairKey}' has conflicting junctionTable values ('${existing.junctionTable}' vs '${incoming.junctionTable}').`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (existing.leftField !== incoming.leftField) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`manyToMany pair '${pairKey}' has conflicting left field values ('${existing.leftField}' vs '${incoming.leftField}').`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (existing.rightField !== incoming.rightField) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`manyToMany pair '${pairKey}' has conflicting right field values ('${existing.rightField}' vs '${incoming.rightField}').`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (existing.leftSqlName !== incoming.leftSqlName) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`manyToMany pair '${pairKey}' has conflicting left sql name values ('${existing.leftSqlName}' vs '${incoming.leftSqlName}').`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (existing.rightSqlName !== incoming.rightSqlName) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`manyToMany pair '${pairKey}' has conflicting right sql name values ('${existing.rightSqlName}' vs '${incoming.rightSqlName}').`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (existing.onDelete !== incoming.onDelete) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`manyToMany pair '${pairKey}' has conflicting onDelete values ('${existing.onDelete}' vs '${incoming.onDelete}').`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (existing.onUpdate !== incoming.onUpdate) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`manyToMany pair '${pairKey}' has conflicting onUpdate values ('${existing.onUpdate}' vs '${incoming.onUpdate}').`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return existing;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeManyToManyRelations(definition: SchemaDefinition): void {
|
|
329
|
+
const pairMap = new Map<string, ManyToManyPair>();
|
|
330
|
+
|
|
331
|
+
for (const [sourceTableName, sourceTable] of Object.entries(
|
|
332
|
+
definition.tables,
|
|
333
|
+
)) {
|
|
334
|
+
for (const relation of Object.values(sourceTable.relations)) {
|
|
335
|
+
if (relation.relation !== "manyToMany") {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const sourceReferenceField = relation.referenceField ?? "id";
|
|
340
|
+
const targetReferenceField = relation.targetReferenceField ?? "id";
|
|
341
|
+
|
|
342
|
+
const normalizedPair = normalizeManyToManyPairKey(
|
|
343
|
+
sourceTableName,
|
|
344
|
+
sourceReferenceField,
|
|
345
|
+
relation.targetTable,
|
|
346
|
+
targetReferenceField,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const defaultLeftField = defaultManyToManyFieldName(
|
|
350
|
+
normalizedPair.leftTable,
|
|
351
|
+
normalizedPair.rightTable,
|
|
352
|
+
"source",
|
|
353
|
+
);
|
|
354
|
+
const defaultRightField = defaultManyToManyFieldName(
|
|
355
|
+
normalizedPair.rightTable,
|
|
356
|
+
normalizedPair.leftTable,
|
|
357
|
+
"target",
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const pair: ManyToManyPair = {
|
|
361
|
+
leftTable: normalizedPair.leftTable,
|
|
362
|
+
leftReferenceField: normalizedPair.leftReferenceField,
|
|
363
|
+
rightTable: normalizedPair.rightTable,
|
|
364
|
+
rightReferenceField: normalizedPair.rightReferenceField,
|
|
365
|
+
junctionTable:
|
|
366
|
+
relation.junctionTable ??
|
|
367
|
+
defaultManyToManyJunctionTable(
|
|
368
|
+
normalizedPair.leftTable,
|
|
369
|
+
normalizedPair.rightTable,
|
|
370
|
+
),
|
|
371
|
+
leftField: normalizedPair.sourceIsLeft
|
|
372
|
+
? (relation.sourceField ?? defaultLeftField)
|
|
373
|
+
: (relation.targetField ?? defaultLeftField),
|
|
374
|
+
rightField: normalizedPair.sourceIsLeft
|
|
375
|
+
? (relation.targetField ?? defaultRightField)
|
|
376
|
+
: (relation.sourceField ?? defaultRightField),
|
|
377
|
+
leftSqlName: normalizedPair.sourceIsLeft
|
|
378
|
+
? relation.sourceSqlName
|
|
379
|
+
: relation.targetSqlName,
|
|
380
|
+
rightSqlName: normalizedPair.sourceIsLeft
|
|
381
|
+
? relation.targetSqlName
|
|
382
|
+
: relation.sourceSqlName,
|
|
383
|
+
onDelete: relation.onDelete,
|
|
384
|
+
onUpdate: relation.onUpdate,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
if (pair.leftField === pair.rightField) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`manyToMany pair '${normalizedPair.key}' resolves to duplicate junction fields '${pair.leftField}'. Set sourceField/targetField explicitly.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const existing = pairMap.get(normalizedPair.key);
|
|
394
|
+
if (existing) {
|
|
395
|
+
mergeManyToManyPairConfig(existing, pair, normalizedPair.key);
|
|
396
|
+
} else {
|
|
397
|
+
pairMap.set(normalizedPair.key, pair);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
relation.referenceField = sourceReferenceField;
|
|
401
|
+
relation.targetReferenceField = targetReferenceField;
|
|
402
|
+
relation.junctionTable = pair.junctionTable;
|
|
403
|
+
relation.sourceField = normalizedPair.sourceIsLeft
|
|
404
|
+
? pair.leftField
|
|
405
|
+
: pair.rightField;
|
|
406
|
+
relation.targetField = normalizedPair.sourceIsLeft
|
|
407
|
+
? pair.rightField
|
|
408
|
+
: pair.leftField;
|
|
409
|
+
relation.sourceSqlName = normalizedPair.sourceIsLeft
|
|
410
|
+
? pair.leftSqlName
|
|
411
|
+
: pair.rightSqlName;
|
|
412
|
+
relation.targetSqlName = normalizedPair.sourceIsLeft
|
|
413
|
+
? pair.rightSqlName
|
|
414
|
+
: pair.leftSqlName;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const pair of pairMap.values()) {
|
|
419
|
+
if (pair.junctionTable in definition.tables) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`manyToMany auto junction table '${pair.junctionTable}' conflicts with an existing table. Set a different junctionTable name.`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const leftType =
|
|
426
|
+
getLocalReferenceType(
|
|
427
|
+
definition,
|
|
428
|
+
pair.leftTable,
|
|
429
|
+
pair.leftReferenceField,
|
|
430
|
+
) ?? "string";
|
|
431
|
+
const rightType =
|
|
432
|
+
getLocalReferenceType(
|
|
433
|
+
definition,
|
|
434
|
+
pair.rightTable,
|
|
435
|
+
pair.rightReferenceField,
|
|
436
|
+
) ?? "string";
|
|
437
|
+
|
|
438
|
+
definition.tables[pair.junctionTable] = {
|
|
439
|
+
kind: "table",
|
|
440
|
+
columns: {
|
|
441
|
+
[pair.leftField]: {
|
|
442
|
+
kind: "column",
|
|
443
|
+
type: leftType,
|
|
444
|
+
sqlName: pair.leftSqlName,
|
|
445
|
+
notNull: true,
|
|
446
|
+
nullable: false,
|
|
447
|
+
references: {
|
|
448
|
+
table: pair.leftTable,
|
|
449
|
+
column: pair.leftReferenceField,
|
|
450
|
+
onDelete: pair.onDelete,
|
|
451
|
+
onUpdate: pair.onUpdate,
|
|
452
|
+
},
|
|
453
|
+
index: true,
|
|
454
|
+
},
|
|
455
|
+
[pair.rightField]: {
|
|
456
|
+
kind: "column",
|
|
457
|
+
type: rightType,
|
|
458
|
+
sqlName: pair.rightSqlName,
|
|
459
|
+
notNull: true,
|
|
460
|
+
nullable: false,
|
|
461
|
+
references: {
|
|
462
|
+
table: pair.rightTable,
|
|
463
|
+
column: pair.rightReferenceField,
|
|
464
|
+
onDelete: pair.onDelete,
|
|
465
|
+
onUpdate: pair.onUpdate,
|
|
466
|
+
},
|
|
467
|
+
index: true,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
relations: {
|
|
471
|
+
[pair.leftTable]: {
|
|
472
|
+
kind: "relation",
|
|
473
|
+
relation: "one",
|
|
474
|
+
targetTable: pair.leftTable,
|
|
475
|
+
field: pair.leftField,
|
|
476
|
+
referenceField: pair.leftReferenceField,
|
|
477
|
+
},
|
|
478
|
+
[pair.rightTable]: {
|
|
479
|
+
kind: "relation",
|
|
480
|
+
relation: "one",
|
|
481
|
+
targetTable: pair.rightTable,
|
|
482
|
+
field: pair.rightField,
|
|
483
|
+
referenceField: pair.rightReferenceField,
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
199
490
|
function validateNullableFlags(definition: SchemaDefinition): void {
|
|
200
491
|
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
201
492
|
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
@@ -207,7 +498,11 @@ function validateNullableFlags(definition: SchemaDefinition): void {
|
|
|
207
498
|
}
|
|
208
499
|
|
|
209
500
|
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
210
|
-
if (
|
|
501
|
+
if (
|
|
502
|
+
relation.relation !== "manyToMany" &&
|
|
503
|
+
relation.notNull === true &&
|
|
504
|
+
relation.nullable === true
|
|
505
|
+
) {
|
|
211
506
|
throw new Error(
|
|
212
507
|
`Invalid nullable configuration on '${tableName}.${relationName}': cannot set both notNull and nullable to true.`,
|
|
213
508
|
);
|
|
@@ -279,6 +574,7 @@ function normalizeSchemaDefinition(
|
|
|
279
574
|
const referenceField = relation.referenceField ?? "id";
|
|
280
575
|
const inferredField =
|
|
281
576
|
relation.field ?? `${singularize(sourceTableName)}Id`;
|
|
577
|
+
relation.field = inferredField;
|
|
282
578
|
const inferredType =
|
|
283
579
|
getLocalReferenceType(normalized, sourceTableName, referenceField) ??
|
|
284
580
|
relation.fkType ??
|
|
@@ -306,6 +602,8 @@ function normalizeSchemaDefinition(
|
|
|
306
602
|
}
|
|
307
603
|
}
|
|
308
604
|
|
|
605
|
+
normalizeManyToManyRelations(normalized);
|
|
606
|
+
|
|
309
607
|
return normalized;
|
|
310
608
|
}
|
|
311
609
|
|
|
@@ -432,6 +730,117 @@ function resolveColumnReference(
|
|
|
432
730
|
};
|
|
433
731
|
}
|
|
434
732
|
|
|
733
|
+
function emitManyToManyRuntimeMetadata(definition: SchemaDefinition): string {
|
|
734
|
+
const tableEntries: string[] = [];
|
|
735
|
+
|
|
736
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
737
|
+
const relationEntries: string[] = [];
|
|
738
|
+
|
|
739
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
740
|
+
if (relation.relation !== "manyToMany") {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!relation.junctionTable) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
relationEntries.push(
|
|
749
|
+
`${quote(relationName)}: {
|
|
750
|
+
targetTable: ${quote(relation.targetTable)},
|
|
751
|
+
junctionTable: ${quote(relation.junctionTable)},
|
|
752
|
+
sourceField: ${quote(relation.sourceField ?? "")},
|
|
753
|
+
targetField: ${quote(relation.targetField ?? "")},
|
|
754
|
+
},`,
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (relationEntries.length === 0) {
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
tableEntries.push(
|
|
763
|
+
`${quote(tableName)}: {
|
|
764
|
+
${relationEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
765
|
+
},`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (tableEntries.length === 0) {
|
|
770
|
+
return "export const __appflareManyToMany = {} as const;\n";
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return `export const __appflareManyToMany = {
|
|
774
|
+
${tableEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
775
|
+
} as const;
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function emitRuntimeRelationMetadata(definition: SchemaDefinition): string {
|
|
780
|
+
const tableEntries: string[] = [];
|
|
781
|
+
|
|
782
|
+
for (const [tableName, table] of Object.entries(definition.tables)) {
|
|
783
|
+
const relationEntries: string[] = [];
|
|
784
|
+
|
|
785
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
786
|
+
if (relation.relation === "one") {
|
|
787
|
+
relationEntries.push(
|
|
788
|
+
`${quote(relationName)}: {
|
|
789
|
+
kind: "one",
|
|
790
|
+
targetTable: ${quote(relation.targetTable)},
|
|
791
|
+
sourceField: ${quote(relation.field ?? "")},
|
|
792
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
793
|
+
},`,
|
|
794
|
+
);
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (relation.relation === "many") {
|
|
799
|
+
relationEntries.push(
|
|
800
|
+
`${quote(relationName)}: {
|
|
801
|
+
kind: "many",
|
|
802
|
+
targetTable: ${quote(relation.targetTable)},
|
|
803
|
+
sourceField: ${quote(relation.field ?? "")},
|
|
804
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
805
|
+
},`,
|
|
806
|
+
);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
relationEntries.push(
|
|
811
|
+
`${quote(relationName)}: {
|
|
812
|
+
kind: "manyToMany",
|
|
813
|
+
targetTable: ${quote(relation.targetTable)},
|
|
814
|
+
junctionTable: ${quote(relation.junctionTable ?? "")},
|
|
815
|
+
sourceField: ${quote(relation.sourceField ?? "")},
|
|
816
|
+
targetField: ${quote(relation.targetField ?? "")},
|
|
817
|
+
referenceField: ${quote(relation.referenceField ?? "id")},
|
|
818
|
+
targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
|
|
819
|
+
},`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (relationEntries.length === 0) {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
tableEntries.push(
|
|
828
|
+
`${quote(tableName)}: {
|
|
829
|
+
${relationEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
830
|
+
},`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (tableEntries.length === 0) {
|
|
835
|
+
return "export const __appflareRelations = {} as const;\n";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return `export const __appflareRelations = {
|
|
839
|
+
${tableEntries.map((entry) => `\t${entry}`).join("\n")}
|
|
840
|
+
} as const;
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
|
|
435
844
|
function emitDrizzleSchema(
|
|
436
845
|
definition: SchemaDefinition,
|
|
437
846
|
strategy: "camelToSnake",
|
|
@@ -536,8 +945,17 @@ function emitDrizzleSchema(
|
|
|
536
945
|
return relation.relation === "many";
|
|
537
946
|
},
|
|
538
947
|
) as Array<[string, ManyRelationDefinition]>;
|
|
948
|
+
const manyToManyRelations = Object.entries(table.relations).filter(
|
|
949
|
+
([, relation]) => {
|
|
950
|
+
return relation.relation === "manyToMany";
|
|
951
|
+
},
|
|
952
|
+
) as Array<[string, ManyToManyRelationDefinition]>;
|
|
539
953
|
|
|
540
|
-
if (
|
|
954
|
+
if (
|
|
955
|
+
oneRelations.length === 0 &&
|
|
956
|
+
manyRelations.length === 0 &&
|
|
957
|
+
manyToManyRelations.length === 0
|
|
958
|
+
) {
|
|
541
959
|
continue;
|
|
542
960
|
}
|
|
543
961
|
|
|
@@ -551,6 +969,14 @@ function emitDrizzleSchema(
|
|
|
551
969
|
for (const [relationName, relation] of manyRelations) {
|
|
552
970
|
relationLines.push(`\t${relationName}: many(${relation.targetTable}),`);
|
|
553
971
|
}
|
|
972
|
+
for (const [relationName, relation] of manyToManyRelations) {
|
|
973
|
+
if (!relation.junctionTable) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
`manyToMany relation '${tableName}.${relationName}' is missing junctionTable after normalization.`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
relationLines.push(`\t${relationName}: many(${relation.junctionTable}),`);
|
|
979
|
+
}
|
|
554
980
|
|
|
555
981
|
relationBlocks.push(
|
|
556
982
|
`export const ${tableName}Relations = relations(${tableName}, ({ one, many }) => ({\n${relationLines.join("\n")}\n}));`,
|
|
@@ -564,6 +990,10 @@ ${buildExternalTableImportLines(externalTables)}
|
|
|
564
990
|
${tableBlocks.join("\n\n")}
|
|
565
991
|
|
|
566
992
|
${relationBlocks.join("\n\n")}
|
|
993
|
+
|
|
994
|
+
${emitManyToManyRuntimeMetadata(definition)}
|
|
995
|
+
|
|
996
|
+
${emitRuntimeRelationMetadata(definition)}
|
|
567
997
|
`;
|
|
568
998
|
}
|
|
569
999
|
|
|
@@ -52,12 +52,15 @@ export function buildColumnHeaders(
|
|
|
52
52
|
.join("");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function buildRowCells(columns: string[]): string {
|
|
55
|
+
export function buildRowCells(columns: string[], primaryKey?: string): string {
|
|
56
56
|
return columns
|
|
57
|
-
.map(
|
|
58
|
-
(column)
|
|
59
|
-
`<td><
|
|
60
|
-
|
|
57
|
+
.map((column) => {
|
|
58
|
+
if (primaryKey && column === primaryKey) {
|
|
59
|
+
return `<td><button type="button" class="truncate max-w-[200px] text-sm font-mono text-xs opacity-70 hover:opacity-100 cursor-copy text-left" title="Click to copy: \${String((row as any).${column} ?? '')}" data-copy-value="\${String((row as any).${column} ?? '')}" onclick="navigator.clipboard?.writeText(this.dataset.copyValue || '')">\${String((row as any).${column} ?? '')}</button></td>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `<td><div class="truncate max-w-[200px] text-sm" title="\${String((row as any).${column} ?? '')}">\${String((row as any).${column} ?? '')}</div></td>`;
|
|
63
|
+
})
|
|
61
64
|
.join("");
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -31,7 +31,7 @@ export function buildTableRoute(table: DiscoveredTable): string {
|
|
|
31
31
|
);
|
|
32
32
|
const searchConditions = buildSearchConditions(table);
|
|
33
33
|
const headers = buildColumnHeaders(table, columns);
|
|
34
|
-
const rowCells = buildRowCells(columns);
|
|
34
|
+
const rowCells = buildRowCells(columns, primaryKey);
|
|
35
35
|
const createInputs = createColumns
|
|
36
36
|
.map((columnName) => buildFieldInput(table, columnName, "create"))
|
|
37
37
|
.join("");
|
|
@@ -55,7 +55,7 @@ export function buildUsersTableHtml(): string {
|
|
|
55
55
|
\t\t\t\t\t\t<tbody>
|
|
56
56
|
\t\t\t\t\t\t\t\${data.map((row) => html\`
|
|
57
57
|
\t\t\t\t\t\t\t\t<tr class="hover:bg-base-200/30 transition-colors">
|
|
58
|
-
\t\t\t\t\t\t\t\t\t<td><
|
|
58
|
+
\t\t\t\t\t\t\t\t\t<td><button type="button" class="truncate max-w-[220px] text-sm font-mono text-xs opacity-70 hover:opacity-100 cursor-copy text-left" title="Click to copy: \${String((row as any).id ?? '')}" data-copy-value="\${String((row as any).id ?? '')}" onclick="navigator.clipboard?.writeText(this.dataset.copyValue || '')">\${String((row as any).id ?? '')}</button></td>
|
|
59
59
|
\t\t\t\t\t\t\t\t\t<td><div class="truncate max-w-[220px] text-sm" title="\${String((row as any).name ?? '')}">\${String((row as any).name ?? '')}</div></td>
|
|
60
60
|
\t\t\t\t\t\t\t\t\t<td><div class="truncate max-w-[260px] text-sm" title="\${String((row as any).email ?? '')}">\${String((row as any).email ?? '')}</div></td>
|
|
61
61
|
\t\t\t\t\t\t\t\t\t<td><span class="badge badge-sm \${String((row as any).role ?? '') === 'admin' ? 'badge-primary' : 'badge-ghost'}">\${String((row as any).role ?? '')}</span></td>
|
|
@@ -123,5 +123,6 @@ ${buildDeleteUserModal()}
|
|
|
123
123
|
\t\t\t\t</div>
|
|
124
124
|
\t\t\t\t${paginationHtml}
|
|
125
125
|
\t\t\t</div>
|
|
126
|
-
\t\t
|
|
126
|
+
\t\t\`;
|
|
127
|
+
`;
|
|
127
128
|
}
|