@appweaver/cli 1.3.1 → 1.4.0
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/generate/generate-schema.js +122 -25
- package/generate/generate-types.js +4 -1
- package/package.json +1 -1
- package/skill/GUIDELINES.md +18 -4
- package/skill/SKILL.md +753 -707
- package/skill/references/cli.md +4 -0
- package/skill/references/client.md +609 -599
- package/skill/references/configuration.md +6 -6
- package/skill/references/resources.md +1424 -1284
- package/skill/references/security.md +29 -5
- package/skill/references/storage.md +38 -4
- package/utils/loader-util.js +3 -0
|
@@ -8,6 +8,22 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
8
8
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
9
|
const common_1 = require("@appweaver/common");
|
|
10
10
|
const utils_1 = require("../utils");
|
|
11
|
+
const INDEX_SORT_PREFIXES = {
|
|
12
|
+
'-': 'Desc',
|
|
13
|
+
'+': 'Asc'
|
|
14
|
+
};
|
|
15
|
+
const UUID_LENGTH = 36;
|
|
16
|
+
const CUID_LENGTHS = {
|
|
17
|
+
'': 25,
|
|
18
|
+
'2': 24
|
|
19
|
+
};
|
|
20
|
+
const NANOID_LENGTH = 21;
|
|
21
|
+
const UUID_NATIVE_TYPES = {
|
|
22
|
+
[common_1.DatabaseType.PostgresSQL]: '@db.Uuid',
|
|
23
|
+
[common_1.DatabaseType.SQLServer]: '@db.UniqueIdentifier',
|
|
24
|
+
[common_1.DatabaseType.MySQL]: `@db.Char(${UUID_LENGTH})`
|
|
25
|
+
};
|
|
26
|
+
const defaultIdColumn = () => ({ type: 'Int' });
|
|
11
27
|
/**
|
|
12
28
|
* Generates a Prisma schema file based on the provided resource models and saves it to the specified file path.
|
|
13
29
|
*
|
|
@@ -58,6 +74,12 @@ async function generateSchema(models, schemaPath, clientPath, quiet = false) {
|
|
|
58
74
|
break;
|
|
59
75
|
}
|
|
60
76
|
}
|
|
77
|
+
// A foreign key column has to match the primary key column it references,
|
|
78
|
+
// native type included, or the database rejects the constraint
|
|
79
|
+
const idColumnOf = (modelName) => {
|
|
80
|
+
const id = models[(0, common_1.capitalize)(modelName)]?.config?.id;
|
|
81
|
+
return { type: prismaIdType(id), nativeType: idNativeType(id) };
|
|
82
|
+
};
|
|
61
83
|
// Create Prisma models and enums from resource model config
|
|
62
84
|
for (const [name, schema] of Object.entries(models)) {
|
|
63
85
|
if (schema.config.generateSchema === false) {
|
|
@@ -66,8 +88,8 @@ async function generateSchema(models, schemaPath, clientPath, quiet = false) {
|
|
|
66
88
|
prismaModels[name] = {
|
|
67
89
|
id: createIdSchema(schema.config.id),
|
|
68
90
|
scalars: createScalarsSchema(name, schema.config.scalars),
|
|
69
|
-
relations: createRelationsSchema(name, schema.config.relations),
|
|
70
|
-
files: createFilesSchema(name, schema.config.files),
|
|
91
|
+
relations: createRelationsSchema(name, schema.config.relations, idColumnOf),
|
|
92
|
+
files: createFilesSchema(name, schema.config.files, idColumnOf('File')),
|
|
71
93
|
audit: createAuditSchema(name, authModel, schema.config.audit),
|
|
72
94
|
index: createIndexSchema(schema.config.index),
|
|
73
95
|
tableName: schema.config.tableName
|
|
@@ -84,7 +106,7 @@ async function generateSchema(models, schemaPath, clientPath, quiet = false) {
|
|
|
84
106
|
for (const [name, model] of Object.entries(prismaModels)) {
|
|
85
107
|
const modelSchema = Object.entries(models).find(([key]) => key === name)?.[1];
|
|
86
108
|
for (const relation of model.relations) {
|
|
87
|
-
if (relation.
|
|
109
|
+
if (relation.foreignKey) {
|
|
88
110
|
continue;
|
|
89
111
|
}
|
|
90
112
|
const relationConfig = modelSchema?.config.relations?.[relation.name];
|
|
@@ -97,7 +119,7 @@ async function generateSchema(models, schemaPath, clientPath, quiet = false) {
|
|
|
97
119
|
.replaceAll('?', '');
|
|
98
120
|
const referencedModel = prismaModels[referencedName];
|
|
99
121
|
const mappedField = referencedModel.relations.find((r) => r.name === relationConfig.mappedBy);
|
|
100
|
-
if (mappedField?.
|
|
122
|
+
if (mappedField?.foreignKey) {
|
|
101
123
|
continue;
|
|
102
124
|
}
|
|
103
125
|
if (mappedField && mappedField.attributes?.length) {
|
|
@@ -136,6 +158,7 @@ async function generateSchema(models, schemaPath, clientPath, quiet = false) {
|
|
|
136
158
|
const refName = (0, common_1.uncapitalize)(name);
|
|
137
159
|
const refFieldName = `${refName}Id`;
|
|
138
160
|
if (!referencedModel.relations.some((r) => r.name === refName)) {
|
|
161
|
+
const idColumn = idColumnOf(name);
|
|
139
162
|
referencedModel.relations.push({
|
|
140
163
|
name: refName,
|
|
141
164
|
type: `${name}?`,
|
|
@@ -145,8 +168,12 @@ async function generateSchema(models, schemaPath, clientPath, quiet = false) {
|
|
|
145
168
|
});
|
|
146
169
|
referencedModel.relations.push({
|
|
147
170
|
name: refFieldName,
|
|
148
|
-
type:
|
|
149
|
-
attributes:
|
|
171
|
+
type: `${idColumn.type}?`,
|
|
172
|
+
attributes: [
|
|
173
|
+
...(relationConfig.type === 'oneToOne' ? ['@unique'] : []),
|
|
174
|
+
...nativeTypeAttributes(idColumn.nativeType)
|
|
175
|
+
],
|
|
176
|
+
foreignKey: true
|
|
150
177
|
});
|
|
151
178
|
}
|
|
152
179
|
}
|
|
@@ -299,15 +326,64 @@ function calculateMaxLengths(fields) {
|
|
|
299
326
|
}
|
|
300
327
|
return { nameLength, typeLength };
|
|
301
328
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
329
|
+
function prismaIdType(id) {
|
|
330
|
+
switch ((0, common_1.idFieldType)(id)) {
|
|
331
|
+
case 'string':
|
|
332
|
+
return 'String';
|
|
333
|
+
case 'bigInt':
|
|
334
|
+
return 'BigInt';
|
|
335
|
+
default:
|
|
336
|
+
return 'Int';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function parseGenerator(generator) {
|
|
340
|
+
const match = generator?.match(/^(\w+)\(([^)]*)\)$/);
|
|
341
|
+
return match ? { name: match[1], arg: match[2].trim() } : undefined;
|
|
342
|
+
}
|
|
343
|
+
function generatorNativeType(generator, dbType = databaseType()) {
|
|
344
|
+
if (dbType === common_1.DatabaseType.Sqlite) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
const parsed = parseGenerator(generator);
|
|
348
|
+
if (!parsed) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
// The argument of `uuid()` and `cuid()` selects the version, the one of
|
|
352
|
+
// `nanoid()` the length of the generated value
|
|
353
|
+
switch (parsed.name) {
|
|
354
|
+
case 'uuid':
|
|
355
|
+
return UUID_NATIVE_TYPES[dbType] ?? `@db.VarChar(${UUID_LENGTH})`;
|
|
356
|
+
case 'cuid':
|
|
357
|
+
return varCharType(CUID_LENGTHS[parsed.arg]);
|
|
358
|
+
case 'nanoid':
|
|
359
|
+
return varCharType(parsed.arg ? Number(parsed.arg) : NANOID_LENGTH, NANOID_LENGTH);
|
|
360
|
+
default:
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function varCharType(length, fallback) {
|
|
365
|
+
const size = Number.isFinite(length) ? length : fallback;
|
|
366
|
+
return size ? `@db.VarChar(${size})` : undefined;
|
|
367
|
+
}
|
|
368
|
+
function idNativeType(id) {
|
|
369
|
+
return (0, common_1.idFieldType)(id) === 'string'
|
|
370
|
+
? generatorNativeType((0, common_1.idFieldGenerator)(id))
|
|
371
|
+
: undefined;
|
|
372
|
+
}
|
|
373
|
+
function createIdSchema(id) {
|
|
305
374
|
return {
|
|
306
375
|
name: 'id',
|
|
307
|
-
type: (
|
|
308
|
-
attributes: [
|
|
376
|
+
type: prismaIdType(id),
|
|
377
|
+
attributes: [
|
|
378
|
+
'@id',
|
|
379
|
+
`@default(${(0, common_1.idFieldGenerator)(id)})`,
|
|
380
|
+
...nativeTypeAttributes(idNativeType(id))
|
|
381
|
+
]
|
|
309
382
|
};
|
|
310
383
|
}
|
|
384
|
+
function nativeTypeAttributes(nativeType) {
|
|
385
|
+
return nativeType ? [nativeType] : [];
|
|
386
|
+
}
|
|
311
387
|
function createScalarsSchema(modelName, scalars = {}) {
|
|
312
388
|
const fields = [];
|
|
313
389
|
for (const [name, scalar] of Object.entries(scalars)) {
|
|
@@ -368,8 +444,13 @@ function createScalarSchema(name, modelName, scalar) {
|
|
|
368
444
|
if (defaultAttribute) {
|
|
369
445
|
attributes.push(defaultAttribute);
|
|
370
446
|
}
|
|
371
|
-
if (scalar.type === 'string' &&
|
|
372
|
-
|
|
447
|
+
if (scalar.type === 'string' && !isSqlite) {
|
|
448
|
+
// The generator width wins over `maxLength`, which only bounds the API input
|
|
449
|
+
const nativeType = generatorNativeType(scalar.defaultGenerator) ??
|
|
450
|
+
varCharType(scalar.maxLength);
|
|
451
|
+
if (nativeType) {
|
|
452
|
+
attributes.push(nativeType);
|
|
453
|
+
}
|
|
373
454
|
}
|
|
374
455
|
return {
|
|
375
456
|
name,
|
|
@@ -430,14 +511,14 @@ function validateRelations(models) {
|
|
|
430
511
|
}
|
|
431
512
|
return errors;
|
|
432
513
|
}
|
|
433
|
-
function createRelationsSchema(modelName, relations = {}) {
|
|
514
|
+
function createRelationsSchema(modelName, relations = {}, idColumnOf = defaultIdColumn) {
|
|
434
515
|
const fields = [];
|
|
435
516
|
for (const [name, relation] of Object.entries(relations)) {
|
|
436
|
-
fields.push(...createRelationSchema(name, modelName, relation));
|
|
517
|
+
fields.push(...createRelationSchema(name, modelName, relation, idColumnOf(relation.model)));
|
|
437
518
|
}
|
|
438
519
|
return fields;
|
|
439
520
|
}
|
|
440
|
-
function createRelationSchema(name, modelName, relation) {
|
|
521
|
+
function createRelationSchema(name, modelName, relation, relationIdColumn = defaultIdColumn()) {
|
|
441
522
|
const attributes = [];
|
|
442
523
|
const relationName = `${modelName}${(0, common_1.capitalize)(name)}${relation.model}`;
|
|
443
524
|
const relationFieldName = `${name}Id`;
|
|
@@ -479,20 +560,24 @@ function createRelationSchema(name, modelName, relation) {
|
|
|
479
560
|
if (owner) {
|
|
480
561
|
relationFields.push({
|
|
481
562
|
name: relationFieldName,
|
|
482
|
-
type:
|
|
483
|
-
attributes:
|
|
563
|
+
type: `${relationIdColumn.type}${relationSuffix}`,
|
|
564
|
+
attributes: [
|
|
565
|
+
...(relation.type === 'oneToOne' ? ['@unique'] : []),
|
|
566
|
+
...nativeTypeAttributes(relationIdColumn.nativeType)
|
|
567
|
+
],
|
|
568
|
+
foreignKey: true
|
|
484
569
|
});
|
|
485
570
|
}
|
|
486
571
|
return relationFields;
|
|
487
572
|
}
|
|
488
|
-
function createFilesSchema(modelName, files = {}) {
|
|
573
|
+
function createFilesSchema(modelName, files = {}, fileIdColumn = defaultIdColumn()) {
|
|
489
574
|
const fields = [];
|
|
490
575
|
for (const [name, file] of Object.entries(files)) {
|
|
491
|
-
fields.push(...createFileSchema(name, modelName, file));
|
|
576
|
+
fields.push(...createFileSchema(name, modelName, file, fileIdColumn));
|
|
492
577
|
}
|
|
493
578
|
return fields;
|
|
494
579
|
}
|
|
495
|
-
function createFileSchema(name, modelName, file) {
|
|
580
|
+
function createFileSchema(name, modelName, file, fileIdColumn = defaultIdColumn()) {
|
|
496
581
|
const attributes = [];
|
|
497
582
|
const type = file.array ? 'File[]' : 'File?';
|
|
498
583
|
const fileRelationName = `${modelName}${(0, common_1.capitalize)(name)}File`;
|
|
@@ -513,8 +598,9 @@ function createFileSchema(name, modelName, file) {
|
|
|
513
598
|
if (!file.array) {
|
|
514
599
|
fileFields.push({
|
|
515
600
|
name: fileRelationFieldName,
|
|
516
|
-
type:
|
|
517
|
-
attributes: ['@unique']
|
|
601
|
+
type: `${fileIdColumn.type}?`,
|
|
602
|
+
attributes: ['@unique', ...nativeTypeAttributes(fileIdColumn.nativeType)],
|
|
603
|
+
foreignKey: true
|
|
518
604
|
});
|
|
519
605
|
}
|
|
520
606
|
return fileFields;
|
|
@@ -552,7 +638,9 @@ function createAuditSchema(modelName, authModel, audit = {}) {
|
|
|
552
638
|
});
|
|
553
639
|
fields.push({
|
|
554
640
|
name: 'createdById',
|
|
555
|
-
type:
|
|
641
|
+
type: `${prismaIdType(authModel?.config.id)}?`,
|
|
642
|
+
attributes: nativeTypeAttributes(idNativeType(authModel?.config.id)),
|
|
643
|
+
foreignKey: true
|
|
556
644
|
});
|
|
557
645
|
}
|
|
558
646
|
return fields;
|
|
@@ -563,7 +651,9 @@ function createIndexSchema(index) {
|
|
|
563
651
|
return indexes;
|
|
564
652
|
}
|
|
565
653
|
for (const idx of index) {
|
|
566
|
-
const indexValue = (0, common_1.isArray)(idx)
|
|
654
|
+
const indexValue = (0, common_1.isArray)(idx)
|
|
655
|
+
? `[${idx.map(indexFieldSchema).join(', ')}]`
|
|
656
|
+
: indexFieldSchema(idx);
|
|
567
657
|
const indexExpression = `@@index(${indexValue})`;
|
|
568
658
|
if (!indexes.includes(indexExpression)) {
|
|
569
659
|
indexes.push(indexExpression);
|
|
@@ -571,6 +661,13 @@ function createIndexSchema(index) {
|
|
|
571
661
|
}
|
|
572
662
|
return indexes;
|
|
573
663
|
}
|
|
664
|
+
function indexFieldSchema(field) {
|
|
665
|
+
const sort = INDEX_SORT_PREFIXES[field.charAt(0)];
|
|
666
|
+
if (!sort || field.length < 2) {
|
|
667
|
+
return field;
|
|
668
|
+
}
|
|
669
|
+
return `${field.slice(1)}(sort: ${sort})`;
|
|
670
|
+
}
|
|
574
671
|
function databaseType() {
|
|
575
672
|
return (0, common_1.resolveDatabaseType)(common_1.config.DATABASE_TYPE, common_1.config.DATABASE_URL);
|
|
576
673
|
}
|
|
@@ -52,7 +52,7 @@ async function generateTypes(models, typesPath, quiet = false) {
|
|
|
52
52
|
``
|
|
53
53
|
];
|
|
54
54
|
if (Object.keys(modelTypeNames).length > 0) {
|
|
55
|
-
typesContent.push(`import { AggregateSelect, QueryFilter, QuerySort } from '@appweaver/common';`, ``);
|
|
55
|
+
typesContent.push(`import { AggregateSelect, IResourceService, QueryFilter, QuerySort } from '@appweaver/common';`, ``);
|
|
56
56
|
}
|
|
57
57
|
for (const [name, typeNames] of Object.entries(modelTypeNames)) {
|
|
58
58
|
for (const typeName of typeNames) {
|
|
@@ -63,6 +63,9 @@ async function generateTypes(models, typesPath, quiet = false) {
|
|
|
63
63
|
// the relations included in a query response and their count fields
|
|
64
64
|
typesContent.push(`export type ${name}Sort = QuerySort<${name}Multiple>;`, ``);
|
|
65
65
|
typesContent.push(`export type ${name}Aggregate = AggregateSelect<${name}>;`, ``);
|
|
66
|
+
// Emitted last, with the arguments making the aliases above exactly the
|
|
67
|
+
// inputs its methods accept
|
|
68
|
+
typesContent.push(`export type ${name}ResourceService = IResourceService<${name}, ${name}Multiple, ${name}Create, ${name}Update, ${name}Query>;`, ``);
|
|
66
69
|
}
|
|
67
70
|
const outputPath = node_path_1.default.join(cwd, typesPath);
|
|
68
71
|
const prettierConfig = await prettier_1.default.resolveConfig(outputPath);
|
package/package.json
CHANGED
package/skill/GUIDELINES.md
CHANGED
|
@@ -84,6 +84,9 @@ export default createModel({
|
|
|
84
84
|
});
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
Index entries are field names, nested in an array for a composite index (`[['status', 'createdAt']]`). Prefix a name
|
|
88
|
+
with `-` for a descending index or `+` for an ascending one (`['-createdAt']`); unprefixed uses the database default.
|
|
89
|
+
|
|
87
90
|
### Service
|
|
88
91
|
|
|
89
92
|
```ts
|
|
@@ -101,6 +104,15 @@ export default createService({
|
|
|
101
104
|
});
|
|
102
105
|
```
|
|
103
106
|
|
|
107
|
+
Inject a resource service anywhere with the `<Model>ResourceService` alias `weaver generate` emits per model:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { injectService } from '@appweaver/core';
|
|
111
|
+
import { ProductResourceService } from '@/types/generated';
|
|
112
|
+
|
|
113
|
+
const products = injectService<ProductResourceService>('Product');
|
|
114
|
+
```
|
|
115
|
+
|
|
104
116
|
### Routes
|
|
105
117
|
|
|
106
118
|
```ts
|
|
@@ -141,9 +153,10 @@ Use `createAuthModel` and `createAuthService` for authenticatable users. They mu
|
|
|
141
153
|
`createAuthModel` adds: `email`, `passwordHash`, `verifiedEmail`, `twoFactorAuth`, `enabled`, `logoutAt` scalars; a
|
|
142
154
|
virtual `password` field; a `roles` relation; and optional `apiKeys` relation.
|
|
143
155
|
|
|
144
|
-
`createAuthService` supports an optional `registrationData` callback to customize the registration payload
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
`createAuthService` supports an optional `registrationData` callback to customize the registration payload, an optional
|
|
157
|
+
`registrationFiles` callback that stores files on the model's file fields right after the user is created (used to keep
|
|
158
|
+
the avatar an OAuth2 provider serves), and an optional `checkOAuth2User` callback to allow or reject OAuth2
|
|
159
|
+
registrations/logins (return nothing to proceed, or a string/`Error` to abort).
|
|
147
160
|
|
|
148
161
|
```ts
|
|
149
162
|
// src/resources/user/model.ts
|
|
@@ -162,7 +175,8 @@ import { createAuthService } from '@appweaver/core';
|
|
|
162
175
|
|
|
163
176
|
export default createAuthService({
|
|
164
177
|
modelName: 'User',
|
|
165
|
-
registrationData: (_, email, password) => ({ email, password, roles: [1, 2] })
|
|
178
|
+
registrationData: (_, email, password) => ({ email, password, roles: [1, 2] }),
|
|
179
|
+
registrationFiles: (_, data) => ({ avatar: data?.avatarFile })
|
|
166
180
|
});
|
|
167
181
|
```
|
|
168
182
|
|