zenstack 0.4.2 → 0.6.0-pre.1
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/{LICENSE.md → LICENSE} +0 -0
- package/bin/cli +1 -1
- package/package.json +18 -13
- package/bin/post-install.js +0 -24
- package/bundle/asset/logo-256-bg.png +0 -0
- package/bundle/asset/logo-dark-256.png +0 -0
- package/bundle/asset/logo-light-256.png +0 -0
- package/bundle/cli/index.js +0 -6952
- package/bundle/extension.js +0 -39
- package/bundle/language-server/main.js +0 -6208
- package/bundle/res/package.template.json +0 -9
- package/bundle/res/prism-zmodel.js +0 -22
- package/bundle/res/stdlib.zmodel +0 -218
- package/bundle/res/tsconfig.template.json +0 -17
- package/src/cli/cli-error.ts +0 -4
- package/src/cli/cli-util.ts +0 -214
- package/src/cli/index.ts +0 -246
- package/src/extension.ts +0 -76
- package/src/generator/ast-utils.ts +0 -18
- package/src/generator/constants.ts +0 -6
- package/src/generator/field-constraint/index.ts +0 -304
- package/src/generator/index.ts +0 -86
- package/src/generator/prisma/expression-writer.ts +0 -360
- package/src/generator/prisma/index.ts +0 -44
- package/src/generator/prisma/prisma-builder.ts +0 -370
- package/src/generator/prisma/query-guard-generator.ts +0 -249
- package/src/generator/prisma/schema-generator.ts +0 -313
- package/src/generator/prisma/typescript-expression-transformer.ts +0 -108
- package/src/generator/react-hooks/index.ts +0 -273
- package/src/generator/service/index.ts +0 -113
- package/src/generator/tsc/index.ts +0 -59
- package/src/generator/types.ts +0 -20
- package/src/global.d.ts +0 -3
- package/src/language-server/constants.ts +0 -29
- package/src/language-server/generated/ast.ts +0 -643
- package/src/language-server/generated/grammar.ts +0 -2492
- package/src/language-server/generated/module.ts +0 -24
- package/src/language-server/langium-ext.d.ts +0 -22
- package/src/language-server/lsp/zmodel-definition-provider.ts +0 -87
- package/src/language-server/main.ts +0 -13
- package/src/language-server/types.ts +0 -25
- package/src/language-server/utils.ts +0 -21
- package/src/language-server/validator/attribute-validator.ts +0 -11
- package/src/language-server/validator/datamodel-validator.ts +0 -426
- package/src/language-server/validator/datasource-validator.ts +0 -102
- package/src/language-server/validator/enum-validator.ts +0 -14
- package/src/language-server/validator/expression-validator.ts +0 -48
- package/src/language-server/validator/schema-validator.ts +0 -31
- package/src/language-server/validator/utils.ts +0 -158
- package/src/language-server/validator/zmodel-validator.ts +0 -91
- package/src/language-server/zmodel-linker.ts +0 -457
- package/src/language-server/zmodel-module.ts +0 -136
- package/src/language-server/zmodel-scope.ts +0 -45
- package/src/language-server/zmodel-workspace-manager.ts +0 -23
- package/src/language-server/zmodel.langium +0 -207
- package/src/res/package.template.json +0 -9
- package/src/res/prism-zmodel.js +0 -22
- package/src/res/stdlib.zmodel +0 -218
- package/src/res/tsconfig.template.json +0 -17
- package/src/telemetry.ts +0 -119
- package/src/utils/exec-utils.ts +0 -8
- package/src/utils/indent-string.ts +0 -9
- package/src/utils/pkg-utils.ts +0 -63
package/src/cli/index.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { paramCase } from 'change-case';
|
|
3
|
-
import colors from 'colors';
|
|
4
|
-
import { Command, Option } from 'commander';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { PackageManagers } from '../utils/pkg-utils';
|
|
7
|
-
import { ZModelLanguageMetaData } from '../language-server/generated/module';
|
|
8
|
-
import telemetry from '../telemetry';
|
|
9
|
-
import { execSync } from '../utils/exec-utils';
|
|
10
|
-
import { CliError } from './cli-error';
|
|
11
|
-
import { initProject, runGenerator } from './cli-util';
|
|
12
|
-
|
|
13
|
-
export const initAction = async (
|
|
14
|
-
projectPath: string,
|
|
15
|
-
options: {
|
|
16
|
-
packageManager: PackageManagers | undefined;
|
|
17
|
-
}
|
|
18
|
-
): Promise<void> => {
|
|
19
|
-
await telemetry.trackSpan(
|
|
20
|
-
'cli:command:start',
|
|
21
|
-
'cli:command:complete',
|
|
22
|
-
'cli:command:error',
|
|
23
|
-
{ command: 'init' },
|
|
24
|
-
() => initProject(projectPath, options.packageManager)
|
|
25
|
-
);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const generateAction = async (options: {
|
|
29
|
-
schema: string;
|
|
30
|
-
packageManager: PackageManagers | undefined;
|
|
31
|
-
}): Promise<void> => {
|
|
32
|
-
await telemetry.trackSpan(
|
|
33
|
-
'cli:command:start',
|
|
34
|
-
'cli:command:complete',
|
|
35
|
-
'cli:command:error',
|
|
36
|
-
{ command: 'generate' },
|
|
37
|
-
() => runGenerator(options)
|
|
38
|
-
);
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function prismaAction(prismaCmd: string): (...args: any[]) => Promise<void> {
|
|
42
|
-
return async (options: any, command: Command) => {
|
|
43
|
-
await telemetry.trackSpan(
|
|
44
|
-
'cli:command:start',
|
|
45
|
-
'cli:command:complete',
|
|
46
|
-
'cli:command:error',
|
|
47
|
-
{
|
|
48
|
-
command: prismaCmd
|
|
49
|
-
? prismaCmd + ' ' + command.name()
|
|
50
|
-
: command.name(),
|
|
51
|
-
},
|
|
52
|
-
async () => {
|
|
53
|
-
const optStr = Array.from(Object.entries<any>(options))
|
|
54
|
-
.map(([k, v]) => {
|
|
55
|
-
let optVal = v;
|
|
56
|
-
if (k === 'schema') {
|
|
57
|
-
optVal = path.join(
|
|
58
|
-
path.dirname(v),
|
|
59
|
-
'schema.prisma'
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
return (
|
|
63
|
-
'--' +
|
|
64
|
-
paramCase(k) +
|
|
65
|
-
(typeof optVal === 'string' ? ` ${optVal}` : '')
|
|
66
|
-
);
|
|
67
|
-
})
|
|
68
|
-
.join(' ');
|
|
69
|
-
|
|
70
|
-
// regenerate prisma schema first
|
|
71
|
-
await runGenerator(options, ['prisma'], false);
|
|
72
|
-
|
|
73
|
-
const prismaExec = `npx prisma ${prismaCmd} ${command.name()} ${optStr}`;
|
|
74
|
-
console.log(prismaExec);
|
|
75
|
-
try {
|
|
76
|
-
execSync(prismaExec);
|
|
77
|
-
} catch {
|
|
78
|
-
telemetry.track('cli:command:error', {
|
|
79
|
-
command: prismaCmd,
|
|
80
|
-
});
|
|
81
|
-
console.error(
|
|
82
|
-
colors.red(
|
|
83
|
-
'Prisma command failed to execute. See errors above.'
|
|
84
|
-
)
|
|
85
|
-
);
|
|
86
|
-
throw new CliError('prisma command run error');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export default async function (): Promise<void> {
|
|
94
|
-
await telemetry.trackSpan(
|
|
95
|
-
'cli:start',
|
|
96
|
-
'cli:complete',
|
|
97
|
-
'cli:error',
|
|
98
|
-
{ args: process.argv },
|
|
99
|
-
async () => {
|
|
100
|
-
const program = new Command('zenstack');
|
|
101
|
-
|
|
102
|
-
program.version(
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
104
|
-
require('../../package.json').version,
|
|
105
|
-
'-v --version',
|
|
106
|
-
'display CLI version'
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const schemaExtensions =
|
|
110
|
-
ZModelLanguageMetaData.fileExtensions.join(', ');
|
|
111
|
-
|
|
112
|
-
program
|
|
113
|
-
.description(
|
|
114
|
-
`${colors.bold.blue(
|
|
115
|
-
'ζ'
|
|
116
|
-
)} ZenStack is a toolkit for building secure CRUD apps with Next.js + Typescript.\n\nDocumentation: https://zenstack.dev.`
|
|
117
|
-
)
|
|
118
|
-
.showHelpAfterError()
|
|
119
|
-
.showSuggestionAfterError();
|
|
120
|
-
|
|
121
|
-
const schemaOption = new Option(
|
|
122
|
-
'--schema <file>',
|
|
123
|
-
`schema file (with extension ${schemaExtensions})`
|
|
124
|
-
).default('./zenstack/schema.zmodel');
|
|
125
|
-
|
|
126
|
-
const pmOption = new Option(
|
|
127
|
-
'-p, --package-manager <pm>',
|
|
128
|
-
'package manager to use'
|
|
129
|
-
).choices(['npm', 'yarn', 'pnpm']);
|
|
130
|
-
|
|
131
|
-
//#region wraps Prisma commands
|
|
132
|
-
|
|
133
|
-
program
|
|
134
|
-
.command('init')
|
|
135
|
-
.description('Set up a new ZenStack project.')
|
|
136
|
-
.addOption(pmOption)
|
|
137
|
-
.argument('[path]', 'project path', '.')
|
|
138
|
-
.action(initAction);
|
|
139
|
-
|
|
140
|
-
program
|
|
141
|
-
.command('generate')
|
|
142
|
-
.description(
|
|
143
|
-
'Generates RESTful API and Typescript client for your data model.'
|
|
144
|
-
)
|
|
145
|
-
.addOption(schemaOption)
|
|
146
|
-
.addOption(pmOption)
|
|
147
|
-
.action(generateAction);
|
|
148
|
-
|
|
149
|
-
const migrate = program
|
|
150
|
-
.command('migrate')
|
|
151
|
-
.description(
|
|
152
|
-
`Updates the database schema with migrations\nAlias for ${colors.cyan(
|
|
153
|
-
'prisma migrate'
|
|
154
|
-
)}.`
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
migrate
|
|
158
|
-
.command('dev')
|
|
159
|
-
.description(
|
|
160
|
-
`Creates a migration, apply it to the database, generate db client\nAlias for ${colors.cyan(
|
|
161
|
-
'prisma migrate dev'
|
|
162
|
-
)}.`
|
|
163
|
-
)
|
|
164
|
-
.addOption(schemaOption)
|
|
165
|
-
.option(
|
|
166
|
-
'--create-only',
|
|
167
|
-
'Create a migration without applying it'
|
|
168
|
-
)
|
|
169
|
-
.option('-n --name <name>', 'Name the migration')
|
|
170
|
-
.option('--skip-seed', 'Skip triggering seed')
|
|
171
|
-
.action(prismaAction('migrate'));
|
|
172
|
-
|
|
173
|
-
migrate
|
|
174
|
-
.command('reset')
|
|
175
|
-
.description(
|
|
176
|
-
`Resets your database and apply all migrations\nAlias for ${colors.cyan(
|
|
177
|
-
'prisma migrate reset'
|
|
178
|
-
)}.`
|
|
179
|
-
)
|
|
180
|
-
.addOption(schemaOption)
|
|
181
|
-
.option('--force', 'Skip the confirmation prompt')
|
|
182
|
-
.action(prismaAction('migrate'));
|
|
183
|
-
|
|
184
|
-
migrate
|
|
185
|
-
.command('deploy')
|
|
186
|
-
.description(
|
|
187
|
-
`Applies pending migrations to the database in production/staging\nAlias for ${colors.cyan(
|
|
188
|
-
'prisma migrate deploy'
|
|
189
|
-
)}.`
|
|
190
|
-
)
|
|
191
|
-
.addOption(schemaOption)
|
|
192
|
-
.action(prismaAction('migrate'));
|
|
193
|
-
|
|
194
|
-
migrate
|
|
195
|
-
.command('status')
|
|
196
|
-
.description(
|
|
197
|
-
`Checks the status of migrations in the production/staging database\nAlias for ${colors.cyan(
|
|
198
|
-
'prisma migrate status'
|
|
199
|
-
)}.`
|
|
200
|
-
)
|
|
201
|
-
.addOption(schemaOption)
|
|
202
|
-
.action(prismaAction('migrate'));
|
|
203
|
-
|
|
204
|
-
const db = program
|
|
205
|
-
.command('db')
|
|
206
|
-
.description(
|
|
207
|
-
`Manages your database schema and lifecycle during development\nAlias for ${colors.cyan(
|
|
208
|
-
'prisma db'
|
|
209
|
-
)}.`
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
db.command('push')
|
|
213
|
-
.description(
|
|
214
|
-
`Pushes the Prisma schema state to the database\nAlias for ${colors.cyan(
|
|
215
|
-
'prisma db push'
|
|
216
|
-
)}.`
|
|
217
|
-
)
|
|
218
|
-
.addOption(schemaOption)
|
|
219
|
-
.option('--accept-data-loss', 'Ignore data loss warnings')
|
|
220
|
-
.action(prismaAction('db'));
|
|
221
|
-
|
|
222
|
-
program
|
|
223
|
-
.command('studio')
|
|
224
|
-
.description(
|
|
225
|
-
`Browses your data with Prisma Studio\nAlias for ${colors.cyan(
|
|
226
|
-
'prisma studio'
|
|
227
|
-
)}.`
|
|
228
|
-
)
|
|
229
|
-
.addOption(schemaOption)
|
|
230
|
-
.option('-p --port <port>', 'Port to start Studio in')
|
|
231
|
-
.option('-b --browser <browser>', 'Browser to open Studio in')
|
|
232
|
-
.option(
|
|
233
|
-
'-n --hostname',
|
|
234
|
-
'Hostname to bind the Express server to'
|
|
235
|
-
)
|
|
236
|
-
.action(prismaAction(''));
|
|
237
|
-
|
|
238
|
-
//#endregion
|
|
239
|
-
|
|
240
|
-
// handle errors explicitly to ensure telemetry
|
|
241
|
-
program.exitOverride();
|
|
242
|
-
|
|
243
|
-
await program.parseAsync(process.argv);
|
|
244
|
-
}
|
|
245
|
-
);
|
|
246
|
-
}
|
package/src/extension.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import * as vscode from 'vscode';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import {
|
|
4
|
-
LanguageClient,
|
|
5
|
-
LanguageClientOptions,
|
|
6
|
-
ServerOptions,
|
|
7
|
-
TransportKind,
|
|
8
|
-
} from 'vscode-languageclient/node';
|
|
9
|
-
|
|
10
|
-
let client: LanguageClient;
|
|
11
|
-
|
|
12
|
-
// This function is called when the extension is activated.
|
|
13
|
-
export function activate(context: vscode.ExtensionContext): void {
|
|
14
|
-
client = startLanguageClient(context);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// This function is called when the extension is deactivated.
|
|
18
|
-
export function deactivate(): Thenable<void> | undefined {
|
|
19
|
-
if (client) {
|
|
20
|
-
return client.stop();
|
|
21
|
-
}
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function startLanguageClient(context: vscode.ExtensionContext): LanguageClient {
|
|
26
|
-
const serverModule = context.asAbsolutePath(
|
|
27
|
-
path.join('bundle', 'language-server', 'main')
|
|
28
|
-
);
|
|
29
|
-
// The debug options for the server
|
|
30
|
-
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging.
|
|
31
|
-
// By setting `process.env.DEBUG_BREAK` to a truthy value, the language server will wait until a debugger is attached.
|
|
32
|
-
const debugOptions = {
|
|
33
|
-
execArgv: [
|
|
34
|
-
'--nolazy',
|
|
35
|
-
`--inspect${process.env.DEBUG_BREAK ? '-brk' : ''}=${
|
|
36
|
-
process.env.DEBUG_SOCKET || '6009'
|
|
37
|
-
}`,
|
|
38
|
-
],
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// If the extension is launched in debug mode then the debug server options are used
|
|
42
|
-
// Otherwise the run options are used
|
|
43
|
-
const serverOptions: ServerOptions = {
|
|
44
|
-
run: { module: serverModule, transport: TransportKind.ipc },
|
|
45
|
-
debug: {
|
|
46
|
-
module: serverModule,
|
|
47
|
-
transport: TransportKind.ipc,
|
|
48
|
-
options: debugOptions,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const fileSystemWatcher =
|
|
53
|
-
vscode.workspace.createFileSystemWatcher('**/*.zmodel');
|
|
54
|
-
context.subscriptions.push(fileSystemWatcher);
|
|
55
|
-
|
|
56
|
-
// Options to control the language client
|
|
57
|
-
const clientOptions: LanguageClientOptions = {
|
|
58
|
-
documentSelector: [{ scheme: 'file', language: 'zmodel' }],
|
|
59
|
-
synchronize: {
|
|
60
|
-
// Notify the server about file changes to files contained in the workspace
|
|
61
|
-
fileEvents: fileSystemWatcher,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Create the language client and start the client.
|
|
66
|
-
const client = new LanguageClient(
|
|
67
|
-
'zmodel',
|
|
68
|
-
'ZenStack Model',
|
|
69
|
-
serverOptions,
|
|
70
|
-
clientOptions
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// Start the client. This will also launch the server
|
|
74
|
-
client.start();
|
|
75
|
-
return client;
|
|
76
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { DataModel, isDataModel, Model } from '@lang/generated/ast';
|
|
2
|
-
import { AstNode, Reference } from 'langium';
|
|
3
|
-
import { GeneratorError } from './types';
|
|
4
|
-
|
|
5
|
-
export function extractDataModelsWithAllowRules(model: Model): DataModel[] {
|
|
6
|
-
return model.declarations.filter(
|
|
7
|
-
(d) =>
|
|
8
|
-
isDataModel(d) &&
|
|
9
|
-
!!d.attributes.find((attr) => attr.decl.ref?.name === '@@allow')
|
|
10
|
-
) as DataModel[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function resolved<T extends AstNode>(ref: Reference<T>): T {
|
|
14
|
-
if (!ref.ref) {
|
|
15
|
-
throw new GeneratorError(`Reference not resolved: ${ref.$refText}`);
|
|
16
|
-
}
|
|
17
|
-
return ref.ref;
|
|
18
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export const RUNTIME_PACKAGE = '@zenstackhq/runtime';
|
|
2
|
-
export const GUARD_FIELD_NAME = 'zenstack_guard';
|
|
3
|
-
export const TRANSACTION_FIELD_NAME = 'zenstack_transaction';
|
|
4
|
-
export const API_ROUTE_NAME = 'zenstack';
|
|
5
|
-
export const GENERATED_CODE_PATH = 'node_modules/.zenstack';
|
|
6
|
-
export const UNKNOWN_USER_ID = 'zenstack_unknown_user';
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DataModel,
|
|
3
|
-
DataModelField,
|
|
4
|
-
DataModelFieldAttribute,
|
|
5
|
-
isDataModel,
|
|
6
|
-
isLiteralExpr,
|
|
7
|
-
LiteralExpr,
|
|
8
|
-
} from '@lang/generated/ast';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import { Project, SourceFile } from 'ts-morph';
|
|
11
|
-
import { Context, Generator } from '../types';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generates field constraint validators (run on both client and server side)
|
|
15
|
-
*/
|
|
16
|
-
export default class FieldConstraintGenerator implements Generator {
|
|
17
|
-
get name() {
|
|
18
|
-
return 'field-constraint';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
get startMessage() {
|
|
22
|
-
return 'Generating field constraints...';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get successMessage() {
|
|
26
|
-
return 'Successfully generated field constraints';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async generate(context: Context) {
|
|
30
|
-
const project = new Project();
|
|
31
|
-
const sf = project.createSourceFile(
|
|
32
|
-
path.join(
|
|
33
|
-
context.generatedCodeDir,
|
|
34
|
-
'src/field-constraint/index.ts'
|
|
35
|
-
),
|
|
36
|
-
undefined,
|
|
37
|
-
{ overwrite: true }
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
sf.addStatements([`import { z } from "zod";`]);
|
|
41
|
-
|
|
42
|
-
context.schema.declarations
|
|
43
|
-
.filter((d): d is DataModel => isDataModel(d))
|
|
44
|
-
.forEach((model) => {
|
|
45
|
-
this.generateConstraints(sf, model);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
sf.formatText();
|
|
49
|
-
await project.save();
|
|
50
|
-
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private generateConstraints(sf: SourceFile, model: DataModel) {
|
|
55
|
-
sf.addStatements(`
|
|
56
|
-
export const ${this.validator(
|
|
57
|
-
model.name,
|
|
58
|
-
'create'
|
|
59
|
-
)}: z.ZodType = z.lazy(() => z.object({
|
|
60
|
-
${model.fields
|
|
61
|
-
.map((f) => ({
|
|
62
|
-
field: f,
|
|
63
|
-
schema: this.makeFieldValidator(f, 'create'),
|
|
64
|
-
}))
|
|
65
|
-
.filter(({ schema }) => !!schema)
|
|
66
|
-
.map(({ field, schema }) => field.name + ': ' + schema)
|
|
67
|
-
.join(',\n')}
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
export const ${this.validator(
|
|
71
|
-
model.name,
|
|
72
|
-
'update'
|
|
73
|
-
)}: z.ZodType = z.lazy(() => z.object({
|
|
74
|
-
${model.fields
|
|
75
|
-
.map((f) => ({
|
|
76
|
-
field: f,
|
|
77
|
-
schema: this.makeFieldValidator(f, 'update'),
|
|
78
|
-
}))
|
|
79
|
-
.filter(({ schema }) => !!schema)
|
|
80
|
-
.map(({ field, schema }) => field.name + ': ' + schema)
|
|
81
|
-
.join(',\n')}
|
|
82
|
-
}).partial());
|
|
83
|
-
`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private makeFieldValidator(
|
|
87
|
-
field: DataModelField,
|
|
88
|
-
mode: 'create' | 'update'
|
|
89
|
-
) {
|
|
90
|
-
const baseSchema = this.makeZodSchema(field, mode);
|
|
91
|
-
let zodSchema = baseSchema;
|
|
92
|
-
|
|
93
|
-
// translate field constraint attributes to zod schema
|
|
94
|
-
for (const attr of field.attributes) {
|
|
95
|
-
switch (attr.decl.ref?.name) {
|
|
96
|
-
case '@length': {
|
|
97
|
-
const min = this.getAttrLiteralArg<number>(attr, 'min');
|
|
98
|
-
if (min) {
|
|
99
|
-
zodSchema += `.min(${min})`;
|
|
100
|
-
}
|
|
101
|
-
const max = this.getAttrLiteralArg<number>(attr, 'max');
|
|
102
|
-
if (max) {
|
|
103
|
-
zodSchema += `.max(${max})`;
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
case '@regex': {
|
|
108
|
-
const expr = this.getAttrLiteralArg<string>(attr, 'regex');
|
|
109
|
-
if (expr) {
|
|
110
|
-
zodSchema += `.regex(/${expr}/)`;
|
|
111
|
-
}
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
case '@startsWith': {
|
|
115
|
-
const text = this.getAttrLiteralArg<string>(attr, 'text');
|
|
116
|
-
if (text) {
|
|
117
|
-
zodSchema += `.startsWith(${JSON.stringify(text)})`;
|
|
118
|
-
}
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
case '@endsWith': {
|
|
122
|
-
const text = this.getAttrLiteralArg<string>(attr, 'text');
|
|
123
|
-
if (text) {
|
|
124
|
-
zodSchema += `.endsWith(${JSON.stringify(text)})`;
|
|
125
|
-
}
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case '@email': {
|
|
129
|
-
zodSchema += `.email()`;
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case '@url': {
|
|
133
|
-
zodSchema += `.url()`;
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
case '@datetime': {
|
|
137
|
-
zodSchema += `.datetime({ offset: true })`;
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
case '@gt': {
|
|
141
|
-
const value = this.getAttrLiteralArg<number>(attr, 'value');
|
|
142
|
-
if (value !== undefined) {
|
|
143
|
-
zodSchema += `.gt(${value})`;
|
|
144
|
-
}
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case '@gte': {
|
|
148
|
-
const value = this.getAttrLiteralArg<number>(attr, 'value');
|
|
149
|
-
if (value !== undefined) {
|
|
150
|
-
zodSchema += `.gte(${value})`;
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
case '@lt': {
|
|
155
|
-
const value = this.getAttrLiteralArg<number>(attr, 'value');
|
|
156
|
-
if (value !== undefined) {
|
|
157
|
-
zodSchema += `.lt(${value})`;
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
case '@lte': {
|
|
162
|
-
const value = this.getAttrLiteralArg<number>(attr, 'value');
|
|
163
|
-
if (value !== undefined) {
|
|
164
|
-
zodSchema += `.lte(${value})`;
|
|
165
|
-
}
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
!isDataModel(field.type.reference?.ref) &&
|
|
173
|
-
zodSchema === baseSchema
|
|
174
|
-
) {
|
|
175
|
-
// empty schema, skip
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (field.type.optional) {
|
|
180
|
-
zodSchema = this.optional(zodSchema);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return zodSchema;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private getAttrLiteralArg<T extends string | number>(
|
|
187
|
-
attr: DataModelFieldAttribute,
|
|
188
|
-
paramName: string
|
|
189
|
-
) {
|
|
190
|
-
const arg = attr.args.find(
|
|
191
|
-
(arg) => arg.$resolvedParam?.name === paramName
|
|
192
|
-
);
|
|
193
|
-
if (!arg || !isLiteralExpr(arg.value)) {
|
|
194
|
-
return undefined;
|
|
195
|
-
}
|
|
196
|
-
return (arg.value as LiteralExpr).value as T;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private makeZodSchema(field: DataModelField, mode: 'create' | 'update') {
|
|
200
|
-
const type = field.type;
|
|
201
|
-
let schema = '';
|
|
202
|
-
if (type.reference && isDataModel(type.reference.ref)) {
|
|
203
|
-
const modelType = type.reference.ref.name;
|
|
204
|
-
const create = this.validator(modelType, 'create');
|
|
205
|
-
const update = this.validator(modelType, 'update');
|
|
206
|
-
|
|
207
|
-
// list all possible action fields in write playload:
|
|
208
|
-
// create/createMany/connectOrCreate/update/updateMany/upsert
|
|
209
|
-
|
|
210
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
|
-
let fields: any = {
|
|
212
|
-
create: this.optional(this.enumerable(create)),
|
|
213
|
-
createMany: this.optional(this.enumerable(create)),
|
|
214
|
-
connectOrCreate: this.optional(
|
|
215
|
-
this.enumerable(this.object({ create }))
|
|
216
|
-
),
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
if (mode === 'update') {
|
|
220
|
-
fields = {
|
|
221
|
-
...fields,
|
|
222
|
-
update: this.optional(
|
|
223
|
-
this.enumerable(
|
|
224
|
-
type.array ? this.object({ data: update }) : update
|
|
225
|
-
)
|
|
226
|
-
),
|
|
227
|
-
updateMany: this.optional(
|
|
228
|
-
this.enumerable(this.object({ data: update }))
|
|
229
|
-
),
|
|
230
|
-
upsert: this.optional(
|
|
231
|
-
type.array
|
|
232
|
-
? this.enumerable(
|
|
233
|
-
this.object({
|
|
234
|
-
create,
|
|
235
|
-
update,
|
|
236
|
-
})
|
|
237
|
-
)
|
|
238
|
-
: this.object({
|
|
239
|
-
create,
|
|
240
|
-
update,
|
|
241
|
-
})
|
|
242
|
-
),
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
schema = this.optional(this.object(fields));
|
|
247
|
-
} else {
|
|
248
|
-
switch (type.type) {
|
|
249
|
-
case 'Int':
|
|
250
|
-
case 'Float':
|
|
251
|
-
case 'Decimal':
|
|
252
|
-
schema = 'z.number()';
|
|
253
|
-
break;
|
|
254
|
-
case 'BigInt':
|
|
255
|
-
schema = 'z.bigint()';
|
|
256
|
-
break;
|
|
257
|
-
case 'String':
|
|
258
|
-
schema = 'z.string()';
|
|
259
|
-
break;
|
|
260
|
-
case 'Boolean':
|
|
261
|
-
schema = 'z.boolean()';
|
|
262
|
-
break;
|
|
263
|
-
case 'DateTime':
|
|
264
|
-
schema = 'z.date()';
|
|
265
|
-
break;
|
|
266
|
-
default:
|
|
267
|
-
schema = 'z.any()';
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (type.array) {
|
|
272
|
-
schema = this.array(schema);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return schema;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private union(...schemas: string[]) {
|
|
280
|
-
return `z.union([${schemas.join(', ')}])`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private optional(schema: string) {
|
|
284
|
-
return `z.optional(${schema})`;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private array(schema: string) {
|
|
288
|
-
return `z.array(${schema})`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private enumerable(schema: string) {
|
|
292
|
-
return this.union(schema, this.array(schema));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
private object(fields: Record<string, string>) {
|
|
296
|
-
return `z.object({ ${Object.entries(fields)
|
|
297
|
-
.map(([k, v]) => k + ': ' + v)
|
|
298
|
-
.join(',\n')} })`;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private validator(modelName: string, mode: 'create' | 'update') {
|
|
302
|
-
return `${modelName}_${mode}_validator`;
|
|
303
|
-
}
|
|
304
|
-
}
|