Skip to content

Commit 8e40d9a

Browse files
committed
fix: continue work on db pull
1 parent 9a95935 commit 8e40d9a

File tree

7 files changed

+270
-76
lines changed

7 files changed

+270
-76
lines changed

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"pack": "pnpm pack"
3030
},
3131
"dependencies": {
32+
"@dotenvx/dotenvx": "^1.51.0",
3233
"@zenstackhq/common-helpers": "workspace:*",
3334
"@zenstackhq/language": "workspace:*",
3435
"@zenstackhq/sdk": "workspace:*",

packages/cli/src/actions/action-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export async function loadSchemaDocument(schemaFile: string) {
4848
}
4949

5050
export async function loadSchemaDocumentWithServices(schemaFile: string) {
51-
const loadResult = await loadDocument(schemaFile);
51+
const loadResult = await loadDocument(schemaFile, [], true);
5252
if (!loadResult.success) {
5353
loadResult.errors.forEach((err) => {
5454
console.error(colors.red(err));

packages/cli/src/actions/db.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ZModelCodeGenerator } from '@zenstackhq/sdk';
22
import fs from 'node:fs';
3+
import path from 'node:path';
34
import { execPackage } from '../utils/exec-utils';
45
import { generateTempPrismaSchema, getSchemaFile, handleSubProcessError, loadSchemaDocumentWithServices } from './action-utils';
56
import { syncEnums, syncRelation, syncTable, type Relation } from './pull';
@@ -14,6 +15,7 @@ type PushOptions = {
1415

1516
type PullOptions = {
1617
schema?: string;
18+
out?: string;
1719
};
1820

1921
/**
@@ -60,7 +62,7 @@ async function runPush(options: PushOptions) {
6062
async function runPull(options: PullOptions) {
6163
const schemaFile = getSchemaFile(options.schema);
6264
const { model, services } = await loadSchemaDocumentWithServices(schemaFile);
63-
65+
await import("@dotenvx/dotenvx/config")
6466
const SUPPORTED_PROVIDERS = ['sqlite', 'postgresql']
6567
const datasource = getDatasource(model)
6668

@@ -82,16 +84,16 @@ async function runPull(options: PullOptions) {
8284

8385
const { enums, tables } = await provider.introspect(datasource.url)
8486

85-
syncEnums(enums, model)
87+
syncEnums({ dbEnums: enums, model, services })
8688

8789
const resolveRelations: Relation[] = []
8890
for (const table of tables) {
89-
const relations = syncTable({ table, model, provider })
91+
const relations = syncTable({ table, model, provider, services })
9092
resolveRelations.push(...relations)
9193
}
9294

93-
for (const rel of resolveRelations) {
94-
syncRelation(model, rel, services);
95+
for (const relation of resolveRelations) {
96+
syncRelation({ model, relation, services });
9597
}
9698

9799
for (const d of model.declarations) {
@@ -104,6 +106,14 @@ async function runPull(options: PullOptions) {
104106

105107
model.declarations = model.declarations.filter((d) => d !== undefined)
106108

107-
const zmpdelSchema = await new ZModelCodeGenerator().generate(model)
108-
fs.writeFileSync(schemaFile, zmpdelSchema)
109+
const generator = await new ZModelCodeGenerator();
110+
111+
const zmodelSchema = await generator.generate(model)
112+
113+
console.log(options.out ? `Writing to ${options.out}` : schemaFile);
114+
115+
const outPath = options.out ? path.resolve(options.out) : schemaFile;
116+
console.log(outPath);
117+
118+
fs.writeFileSync(outPath, zmodelSchema)
109119
}

packages/cli/src/actions/pull/index.ts

Lines changed: 68 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ZModelServices } from '@zenstackhq/language'
22
import type {
3+
ArrayExpr,
34
Attribute,
45
AttributeArg,
56
DataField,
@@ -9,18 +10,20 @@ import type {
910
Enum,
1011
EnumField,
1112
Model,
13+
ReferenceExpr,
14+
StringLiteral,
1215
UnsupportedFieldType
1316
} from '@zenstackhq/language/ast'
17+
import { getStringLiteral } from '@zenstackhq/language/utils'
1418
import type { IntrospectedEnum, IntrospectedTable, IntrospectionProvider } from './provider'
15-
import { getAttributeRef, getDbName } from './utils'
19+
import { getAttributeRef, getDbName, getEnumRef, getModelRef } from './utils'
1620

17-
export function syncEnums(dbEnums: IntrospectedEnum[], model: Model) {
21+
export function syncEnums({ dbEnums, model, services }: { dbEnums: IntrospectedEnum[], model: Model, services: ZModelServices }) {
1822
for (const dbEnum of dbEnums) {
19-
let schemaEnum = model.declarations.find(
20-
(d) => d.$type === 'Enum' && getDbName(d) === dbEnum.enum_type
21-
) as Enum | undefined
23+
let schemaEnum = getEnumRef(dbEnum.enum_type, services);
2224

2325
if (!schemaEnum) {
26+
console.log(`Adding enum for type ${dbEnum.enum_type}`);
2427
schemaEnum = {
2528
$type: 'Enum' as const,
2629
$container: model,
@@ -66,17 +69,29 @@ export function syncTable({
6669
model,
6770
provider,
6871
table,
72+
services
6973
}: {
7074
table: IntrospectedTable
7175
model: Model
7276
provider: IntrospectionProvider
77+
services: ZModelServices
7378
}) {
79+
const idAttribute = getAttributeRef('@id', services)
80+
const uniqueAttribute = getAttributeRef('@unique', services)
81+
const relationAttribute = getAttributeRef('@relation', services)
82+
const fieldMapAttribute = getAttributeRef('@map', services)
83+
const tableMapAttribute = getAttributeRef('@@map', services)
84+
85+
if (!idAttribute || !uniqueAttribute || !relationAttribute || !fieldMapAttribute || !tableMapAttribute) {
86+
throw new Error('Cannot find required attributes in the model.')
87+
}
88+
7489
const relations: Relation[] = []
75-
let modelTable = model.declarations.find(
76-
(d) => d.$type === 'DataModel' && getDbName(d) === table.name
77-
) as DataModel | undefined
90+
let modelTable = getModelRef(table.name, services)
7891

7992
if (!modelTable) {
93+
console.log(`Adding model for table ${table.name}`);
94+
8095
modelTable = {
8196
$type: 'DataModel' as const,
8297
$container: model,
@@ -96,7 +111,7 @@ export function syncTable({
96111
schema: table.schema,
97112
table: table.name,
98113
column: col.name,
99-
type: col.unique ? 'one' : 'many',
114+
type: 'one',
100115
fk_name: col.foreign_key_name!,
101116
nullable: col.nullable,
102117
references: {
@@ -115,85 +130,68 @@ export function syncTable({
115130
)
116131
if (!existingField) {
117132
const builtinType = provider.getBuiltinType(col.datatype)
118-
const unsupported: UnsupportedFieldType = {
119-
get $container() {
120-
return type
121-
},
122-
$type: 'UnsupportedFieldType' as const,
123-
value: {
124-
get $container() {
125-
return unsupported
126-
},
127-
$type: 'StringLiteral',
128-
value: col.datatype,
129-
},
130-
}
131-
132-
const type: DataFieldType = {
133-
get $container() {
134-
return field
135-
},
136-
$type: 'DataFieldType' as const,
137-
type: builtinType.type === 'Unsupported' ? undefined : builtinType.type,
138-
array: builtinType.isArray,
139-
unsupported:
140-
builtinType.type === 'Unsupported' ? unsupported : undefined,
141-
optional: col.nullable,
142-
reference: col.options.length
143-
? {
133+
const field: DataField = {
134+
$type: 'DataField' as const,
135+
get type() {
136+
return {
137+
$container: this,
138+
$type: 'DataFieldType' as const,
139+
type: builtinType.type === 'Unsupported' ? undefined : builtinType.type,
140+
array: builtinType.isArray,
141+
get unsupported() {
142+
return builtinType.type === 'Unsupported' ? {
143+
$container: this,
144+
$type: 'UnsupportedFieldType' as const,
145+
get value() {
146+
return {
147+
$container: this,
148+
$type: 'StringLiteral',
149+
value: col.datatype,
150+
} satisfies StringLiteral
151+
},
152+
} satisfies UnsupportedFieldType : undefined
153+
},
154+
optional: col.nullable,
155+
reference: col.options.length
156+
? {
144157
$refText: col.datatype,
145158
ref: model.declarations.find(
146159
(d) => d.$type === 'Enum' && getDbName(d) === col.datatype
147-
) as Enum | undefined,
148-
}
149-
: undefined,
150-
}
151-
152-
const field: DataField = {
153-
$type: 'DataField' as const,
154-
type,
160+
) as Enum | undefined,
161+
}
162+
: undefined,
163+
} satisfies DataFieldType
164+
},
155165
$container: modelTable!,
156166
name: fieldName,
157167
get attributes() {
158168
if (fieldPrefix !== '') return []
159169

160-
const attr: DataFieldAttribute = {
170+
return [{
161171
$type: 'DataFieldAttribute' as const,
162-
get $container() {
163-
return field
164-
},
172+
$container: this,
165173
decl: {
166174
$refText: '@map',
167-
ref: model.$document?.references.find(
168-
(r) =>
169-
//@ts-ignore
170-
r.ref.$type === 'Attribute' && r.ref.name === '@map'
171-
)?.ref as Attribute,
175+
ref: fieldMapAttribute,
172176
},
173177
get args() {
174-
const arg: AttributeArg = {
178+
return [{
175179
$type: 'AttributeArg' as const,
176-
get $container() {
177-
return attr
178-
},
180+
$container: this,
179181
name: 'name',
180182
$resolvedParam: {
181183
name: 'name',
182184
},
183185
get value() {
184186
return {
185187
$type: 'StringLiteral' as const,
186-
$container: arg,
188+
$container: this,
187189
value: col.name,
188190
}
189191
},
190-
}
191-
192-
return [arg]
192+
}] satisfies AttributeArg[]
193193
},
194-
}
195-
196-
return [attr]
194+
}] satisfies DataFieldAttribute[]
197195
},
198196
comments: [],
199197
}
@@ -205,10 +203,16 @@ export function syncTable({
205203
return relations
206204
}
207205

208-
export function syncRelation(model: Model, relation: Relation, services: ZModelServices) {
206+
export function syncRelation({ model, relation, services }: { model: Model, relation: Relation, services: ZModelServices }) {
209207
const idAttribute = getAttributeRef('@id', services)
210208
const uniqueAttribute = getAttributeRef('@unique', services)
211209
const relationAttribute = getAttributeRef('@relation', services)
210+
const fieldMapAttribute = getAttributeRef('@map', services)
211+
const tableMapAttribute = getAttributeRef('@@map', services)
212+
213+
if (!idAttribute || !uniqueAttribute || !relationAttribute || !fieldMapAttribute || !tableMapAttribute) {
214+
throw new Error('Cannot find required attributes in the model.')
215+
}
212216

213217
if (!idAttribute || !uniqueAttribute || !relationAttribute) {
214218
throw new Error('Cannot find required attributes in the model.')

packages/cli/src/actions/pull/utils.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { ZModelServices } from '@zenstackhq/language'
22
import {
3+
AbstractDeclaration,
34
DataField,
5+
DataModel,
6+
Enum,
47
EnumField,
58
isInvocationExpr,
6-
type AbstractDeclaration,
79
type Attribute,
8-
type Model,
10+
type Model
911
} from '@zenstackhq/language/ast'
1012
import { getStringLiteral } from '@zenstackhq/language/utils'
1113
import type {
@@ -28,10 +30,20 @@ export function getDatasource(model: Model) {
2830
}
2931

3032
const urlField = datasource.fields.find((f) => f.name === 'url')!
33+
3134
let url = getStringLiteral(urlField.value)
3235

3336
if (!url && isInvocationExpr(urlField.value)) {
34-
url = process.env[getStringLiteral(urlField.value.args[0]) as string]!
37+
const envName = getStringLiteral(urlField.value.args[0]?.value)
38+
if (!envName) {
39+
throw new Error('The url field must be a string literal or an env().')
40+
}
41+
if (!process.env[envName]) {
42+
throw new Error(
43+
`Environment variable ${envName} is not set, please set it to the database connection string.`
44+
)
45+
}
46+
url = process.env[envName]
3547
}
3648

3749
if (!url) {
@@ -62,6 +74,19 @@ export function getDbName(
6274
return attrValue.value
6375
}
6476

77+
78+
export function getDeclarationRef<T extends AbstractDeclaration>(type: T["$type"], name: string, services: ZModelServices) {
79+
return services.shared.workspace.IndexManager.allElements(type).find((m) => m.node && getDbName(m.node as T) === name)?.node as T | undefined
80+
}
81+
82+
export function getEnumRef(name: string, services: ZModelServices) {
83+
return getDeclarationRef<Enum>('Enum', name, services);
84+
}
85+
86+
export function getModelRef(name: string, services: ZModelServices) {
87+
return getDeclarationRef<DataModel>('DataModel', name, services);
88+
}
89+
6590
export function getAttributeRef(name: string, services: ZModelServices) {
66-
return services.shared.workspace.IndexManager.allElements("Attribute").find(a => a.name === name) as Attribute | undefined
91+
return getDeclarationRef<Attribute>('Attribute', name, services);
6792
}

packages/cli/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ function createProgram() {
121121
.addOption(new Option('--force-reset', 'force a reset of the database before push'))
122122
.action((options) => dbAction('push', options));
123123

124+
dbCommand
125+
.command('pull')
126+
.description('Introspect your database.')
127+
.addOption(schemaOption)
128+
.addOption(noVersionCheckOption)
129+
.addOption(new Option('--out <path>', 'add custom output path for the introspected schema'))
130+
.action((options) => dbAction('pull', options));
131+
124132
program
125133
.command('info')
126134
.description('Get information of installed ZenStack packages.')

0 commit comments

Comments
 (0)