diff --git a/package.json b/package.json index 13ab2c6e..389a40dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index f642875f..e3f9f8f7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index 87891172..4d94bf5f 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 3a9bed10..248e7104 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index 6dd12fd4..4f938de5 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index d02cdeee..3395da97 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "type": "module", "private": true, "license": "MIT" diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 912d493b..74b326e1 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack", "publisher": "zenstack", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "displayName": "ZenStack Language Tools", "description": "VSCode extension for ZenStack ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index bb55ec00..ab590aca 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 246d12b7..8c768bc0 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "ZenStack Runtime", "type": "module", "scripts": { @@ -65,11 +65,12 @@ } }, "dependencies": { - "@zenstackhq/common-helpers": "workspace:*", "@paralleldrive/cuid2": "^2.2.2", + "@zenstackhq/common-helpers": "workspace:*", "decimal.js": "^10.4.3", "json-stable-stringify": "^1.3.0", "nanoid": "^5.0.9", + "toposort": "^2.0.2", "ts-pattern": "catalog:", "ulid": "^3.0.0", "uuid": "^11.0.5" @@ -91,6 +92,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/pg": "^8.0.0", + "@types/toposort": "^2.0.7", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index 0f42d757..4978dedf 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -1,12 +1,12 @@ import { invariant } from '@zenstackhq/common-helpers'; import { CreateTableBuilder, sql, type ColumnDataType, type OnModifyForeignAction } from 'kysely'; +import toposort from 'toposort'; import { match } from 'ts-pattern'; import { ExpressionUtils, type BuiltinType, type CascadeAction, type FieldDef, - type GetModels, type ModelDef, type SchemaDef, } from '../../schema'; @@ -24,32 +24,84 @@ export class SchemaDbPusher { if (this.schema.enums && this.schema.provider.type === 'postgresql') { for (const [name, enumDef] of Object.entries(this.schema.enums)) { const createEnum = tx.schema.createType(name).asEnum(Object.values(enumDef)); - // console.log('Creating enum:', createEnum.compile().sql); await createEnum.execute(); } } - for (const model of Object.keys(this.schema.models)) { - const createTable = this.createModelTable(tx, model as GetModels); - // console.log('Creating table:', createTable.compile().sql); + // sort models so that target of fk constraints are created first + const sortedModels = this.sortModels(this.schema.models); + for (const modelDef of sortedModels) { + const createTable = this.createModelTable(tx, modelDef); await createTable.execute(); } }); } - private createModelTable(kysely: ToKysely, model: GetModels) { - let table = kysely.schema.createTable(model).ifNotExists(); - const modelDef = requireModel(this.schema, model); + private sortModels(models: Record): ModelDef[] { + const graph: [ModelDef, ModelDef | undefined][] = []; + + for (const model of Object.values(models)) { + let added = false; + + if (model.baseModel) { + // base model should be created before concrete model + const baseDef = requireModel(this.schema, model.baseModel); + // edge: concrete model -> base model + graph.push([model, baseDef]); + added = true; + } + + for (const field of Object.values(model.fields)) { + // relation order + if (field.relation && field.relation.fields && field.relation.references) { + const targetModel = requireModel(this.schema, field.type); + // edge: fk side -> target model + graph.push([model, targetModel]); + added = true; + } + } + + if (!added) { + // no relations, add self to graph to ensure it is included in the result + graph.push([model, undefined]); + } + } + + return toposort(graph) + .reverse() + .filter((m) => !!m); + } + + private createModelTable(kysely: ToKysely, modelDef: ModelDef) { + let table: CreateTableBuilder = kysely.schema.createTable(modelDef.name).ifNotExists(); + for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { + if (fieldDef.originModel && !fieldDef.id) { + // skip non-id fields inherited from base model + continue; + } + if (fieldDef.relation) { - table = this.addForeignKeyConstraint(table, model, fieldName, fieldDef); + table = this.addForeignKeyConstraint(table, modelDef.name, fieldName, fieldDef); } else if (!this.isComputedField(fieldDef)) { - table = this.createModelField(table, fieldName, fieldDef, modelDef); + table = this.createModelField(table, fieldDef, modelDef); } } - table = this.addPrimaryKeyConstraint(table, model, modelDef); - table = this.addUniqueConstraint(table, model, modelDef); + if (modelDef.baseModel) { + // create fk constraint + const baseModelDef = requireModel(this.schema, modelDef.baseModel); + table = table.addForeignKeyConstraint( + `fk_${modelDef.baseModel}_delegate`, + baseModelDef.idFields, + modelDef.baseModel, + baseModelDef.idFields, + (cb) => cb.onDelete('cascade').onUpdate('cascade'), + ); + } + + table = this.addPrimaryKeyConstraint(table, modelDef); + table = this.addUniqueConstraint(table, modelDef); return table; } @@ -58,11 +110,7 @@ export class SchemaDbPusher { return fieldDef.attributes?.some((a) => a.name === '@computed'); } - private addPrimaryKeyConstraint( - table: CreateTableBuilder, - model: GetModels, - modelDef: ModelDef, - ) { + private addPrimaryKeyConstraint(table: CreateTableBuilder, modelDef: ModelDef) { if (modelDef.idFields.length === 1) { if (Object.values(modelDef.fields).some((f) => f.id)) { // @id defined at field level @@ -71,13 +119,13 @@ export class SchemaDbPusher { } if (modelDef.idFields.length > 0) { - table = table.addPrimaryKeyConstraint(`pk_${model}`, modelDef.idFields); + table = table.addPrimaryKeyConstraint(`pk_${modelDef.name}`, modelDef.idFields); } return table; } - private addUniqueConstraint(table: CreateTableBuilder, model: string, modelDef: ModelDef) { + private addUniqueConstraint(table: CreateTableBuilder, modelDef: ModelDef) { for (const [key, value] of Object.entries(modelDef.uniqueFields)) { invariant(typeof value === 'object', 'expecting an object'); if ('type' in value) { @@ -86,22 +134,17 @@ export class SchemaDbPusher { if (fieldDef.unique) { continue; } - table = table.addUniqueConstraint(`unique_${model}_${key}`, [key]); + table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, [key]); } else { // multi-field constraint - table = table.addUniqueConstraint(`unique_${model}_${key}`, Object.keys(value)); + table = table.addUniqueConstraint(`unique_${modelDef.name}_${key}`, Object.keys(value)); } } return table; } - private createModelField( - table: CreateTableBuilder, - fieldName: string, - fieldDef: FieldDef, - modelDef: ModelDef, - ) { - return table.addColumn(fieldName, this.mapFieldType(fieldDef), (col) => { + private createModelField(table: CreateTableBuilder, fieldDef: FieldDef, modelDef: ModelDef) { + return table.addColumn(fieldDef.name, this.mapFieldType(fieldDef), (col) => { // @id if (fieldDef.id && modelDef.idFields.length === 1) { col = col.primaryKey(); @@ -178,7 +221,7 @@ export class SchemaDbPusher { private addForeignKeyConstraint( table: CreateTableBuilder, - model: GetModels, + model: string, fieldName: string, fieldDef: FieldDef, ) { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 05c92085..2b8f185b 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index c561aad0..0a2aeeb4 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index f065f334..61f4544e 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index 012aa924..a786fd7e 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index d7c147ff..ccd6fc4b 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index a5db7cd9..a6665f96 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "", "type": "module", "main": "index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf8203d5..a133573a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,9 @@ importers: pg: specifier: ^8.13.1 version: 8.13.1 + toposort: + specifier: ^2.0.2 + version: 2.0.2 ts-pattern: specifier: 'catalog:' version: 5.7.1 @@ -294,6 +297,9 @@ importers: '@types/pg': specifier: ^8.0.0 version: 8.11.11 + '@types/toposort': + specifier: ^2.0.7 + version: 2.0.7 '@zenstackhq/eslint-config': specifier: workspace:* version: link:../eslint-config @@ -1116,6 +1122,9 @@ packages: '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + '@types/toposort@2.0.7': + resolution: {integrity: sha512-sQNk65vbC36+UixCkcky+dCr7MlflHcVILg1FVGqlUntsLFv9xd9ToWIVko/gTuin+cVe16t+2YubEFkhnSuPQ==} + '@types/vscode@1.101.0': resolution: {integrity: sha512-ZWf0IWa+NGegdW3iU42AcDTFHWW7fApLdkdnBqwYEtHVIBGbTu0ZNQKP/kX3Ds/uMJXIMQNAojHR4vexCEEz5Q==} @@ -2304,6 +2313,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -3032,6 +3044,8 @@ snapshots: '@types/tmp@0.2.6': {} + '@types/toposort@2.0.7': {} + '@types/vscode@1.101.0': {} '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': @@ -4278,6 +4292,8 @@ snapshots: dependencies: is-number: 7.0.0 + toposort@2.0.2: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 diff --git a/samples/blog/package.json b/samples/blog/package.json index 9bdff353..803d7ddd 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "description": "", "main": "index.js", "scripts": { diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 60d266f4..2d5b272f 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-alpha.21", + "version": "3.0.0-alpha.22", "private": true, "type": "module", "scripts": {