Skip to content

Commit e87ad18

Browse files
committed
feat: let schema pusher support delegate models
1 parent c28fe54 commit e87ad18

File tree

3 files changed

+89
-30
lines changed

3 files changed

+89
-30
lines changed

packages/runtime/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@
6565
}
6666
},
6767
"dependencies": {
68-
"@zenstackhq/common-helpers": "workspace:*",
6968
"@paralleldrive/cuid2": "^2.2.2",
69+
"@zenstackhq/common-helpers": "workspace:*",
7070
"decimal.js": "^10.4.3",
7171
"json-stable-stringify": "^1.3.0",
7272
"nanoid": "^5.0.9",
73+
"toposort": "^2.0.2",
7374
"ts-pattern": "catalog:",
7475
"ulid": "^3.0.0",
7576
"uuid": "^11.0.5"
@@ -91,6 +92,7 @@
9192
"devDependencies": {
9293
"@types/better-sqlite3": "^7.6.13",
9394
"@types/pg": "^8.0.0",
95+
"@types/toposort": "^2.0.7",
9496
"@zenstackhq/eslint-config": "workspace:*",
9597
"@zenstackhq/language": "workspace:*",
9698
"@zenstackhq/sdk": "workspace:*",

packages/runtime/src/client/helpers/schema-db-pusher.ts

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { invariant } from '@zenstackhq/common-helpers';
22
import { CreateTableBuilder, sql, type ColumnDataType, type OnModifyForeignAction } from 'kysely';
3+
import toposort from 'toposort';
34
import { match } from 'ts-pattern';
45
import {
56
ExpressionUtils,
67
type BuiltinType,
78
type CascadeAction,
89
type FieldDef,
9-
type GetModels,
1010
type ModelDef,
1111
type SchemaDef,
1212
} from '../../schema';
@@ -24,32 +24,82 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
2424
if (this.schema.enums && this.schema.provider.type === 'postgresql') {
2525
for (const [name, enumDef] of Object.entries(this.schema.enums)) {
2626
const createEnum = tx.schema.createType(name).asEnum(Object.values(enumDef));
27-
// console.log('Creating enum:', createEnum.compile().sql);
2827
await createEnum.execute();
2928
}
3029
}
3130

32-
for (const model of Object.keys(this.schema.models)) {
33-
const createTable = this.createModelTable(tx, model as GetModels<Schema>);
34-
// console.log('Creating table:', createTable.compile().sql);
31+
// sort models so that target of fk constraints are created first
32+
const sortedModels = this.sortModels(this.schema.models);
33+
for (const modelDef of sortedModels) {
34+
const createTable = this.createModelTable(tx, modelDef);
3535
await createTable.execute();
3636
}
3737
});
3838
}
3939

40-
private createModelTable(kysely: ToKysely<Schema>, model: GetModels<Schema>) {
41-
let table = kysely.schema.createTable(model).ifNotExists();
42-
const modelDef = requireModel(this.schema, model);
40+
private sortModels(models: Record<string, ModelDef>): ModelDef[] {
41+
const graph: [ModelDef, ModelDef | undefined][] = [];
42+
43+
for (const model of Object.values(models)) {
44+
let added = false;
45+
46+
if (model.baseModel) {
47+
// base model should be created before concrete model
48+
const baseDef = requireModel(this.schema, model.baseModel);
49+
// edge: base model -> concrete model
50+
graph.push([baseDef, model]);
51+
added = true;
52+
}
53+
54+
for (const field of Object.values(model.fields)) {
55+
// relation order
56+
if (field.relation && field.relation.fields && field.relation.references) {
57+
const targetModel = requireModel(this.schema, field.type);
58+
// edge: relation target model -> fk model
59+
graph.push([targetModel, model]);
60+
added = true;
61+
}
62+
}
63+
64+
if (!added) {
65+
// no relations, add self to graph to ensure it is included in the result
66+
graph.push([model, undefined]);
67+
}
68+
}
69+
70+
return toposort(graph).filter((m) => !!m);
71+
}
72+
73+
private createModelTable(kysely: ToKysely<Schema>, modelDef: ModelDef) {
74+
let table: CreateTableBuilder<string, any> = kysely.schema.createTable(modelDef.name).ifNotExists();
75+
4376
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
77+
if (fieldDef.originModel && !fieldDef.id) {
78+
// skip non-id fields inherited from base model
79+
continue;
80+
}
81+
4482
if (fieldDef.relation) {
45-
table = this.addForeignKeyConstraint(table, model, fieldName, fieldDef);
83+
table = this.addForeignKeyConstraint(table, modelDef.name, fieldName, fieldDef);
4684
} else if (!this.isComputedField(fieldDef)) {
47-
table = this.createModelField(table, fieldName, fieldDef, modelDef);
85+
table = this.createModelField(table, fieldDef, modelDef);
4886
}
4987
}
5088

51-
table = this.addPrimaryKeyConstraint(table, model, modelDef);
52-
table = this.addUniqueConstraint(table, model, modelDef);
89+
if (modelDef.baseModel) {
90+
// create fk constraint
91+
const baseModelDef = requireModel(this.schema, modelDef.baseModel);
92+
table = table.addForeignKeyConstraint(
93+
`fk_${modelDef.baseModel}_delegate`,
94+
baseModelDef.idFields,
95+
modelDef.baseModel,
96+
baseModelDef.idFields,
97+
(cb) => cb.onDelete('cascade').onUpdate('cascade'),
98+
);
99+
}
100+
101+
table = this.addPrimaryKeyConstraint(table, modelDef);
102+
table = this.addUniqueConstraint(table, modelDef);
53103

54104
return table;
55105
}
@@ -58,11 +108,7 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
58108
return fieldDef.attributes?.some((a) => a.name === '@computed');
59109
}
60110

61-
private addPrimaryKeyConstraint(
62-
table: CreateTableBuilder<string, any>,
63-
model: GetModels<Schema>,
64-
modelDef: ModelDef,
65-
) {
111+
private addPrimaryKeyConstraint(table: CreateTableBuilder<string, any>, modelDef: ModelDef) {
66112
if (modelDef.idFields.length === 1) {
67113
if (Object.values(modelDef.fields).some((f) => f.id)) {
68114
// @id defined at field level
@@ -71,13 +117,13 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
71117
}
72118

73119
if (modelDef.idFields.length > 0) {
74-
table = table.addPrimaryKeyConstraint(`pk_${model}`, modelDef.idFields);
120+
table = table.addPrimaryKeyConstraint(`pk_${modelDef.name}`, modelDef.idFields);
75121
}
76122

77123
return table;
78124
}
79125

80-
private addUniqueConstraint(table: CreateTableBuilder<string, any>, model: string, modelDef: ModelDef) {
126+
private addUniqueConstraint(table: CreateTableBuilder<string, any>, modelDef: ModelDef) {
81127
for (const [key, value] of Object.entries(modelDef.uniqueFields)) {
82128
invariant(typeof value === 'object', 'expecting an object');
83129
if ('type' in value) {
@@ -86,22 +132,17 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
86132
if (fieldDef.unique) {
87133
continue;
88134
}
89-
table = table.addUniqueConstraint(`unique_${model}_${key}`, [key]);
135+
table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, [key]);
90136
} else {
91137
// multi-field constraint
92-
table = table.addUniqueConstraint(`unique_${model}_${key}`, Object.keys(value));
138+
table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, Object.keys(value));
93139
}
94140
}
95141
return table;
96142
}
97143

98-
private createModelField(
99-
table: CreateTableBuilder<any>,
100-
fieldName: string,
101-
fieldDef: FieldDef,
102-
modelDef: ModelDef,
103-
) {
104-
return table.addColumn(fieldName, this.mapFieldType(fieldDef), (col) => {
144+
private createModelField(table: CreateTableBuilder<any>, fieldDef: FieldDef, modelDef: ModelDef) {
145+
return table.addColumn(fieldDef.name, this.mapFieldType(fieldDef), (col) => {
105146
// @id
106147
if (fieldDef.id && modelDef.idFields.length === 1) {
107148
col = col.primaryKey();
@@ -178,7 +219,7 @@ export class SchemaDbPusher<Schema extends SchemaDef> {
178219

179220
private addForeignKeyConstraint(
180221
table: CreateTableBuilder<string, any>,
181-
model: GetModels<Schema>,
222+
model: string,
182223
fieldName: string,
183224
fieldDef: FieldDef,
184225
) {

pnpm-lock.yaml

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)