@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.
@@ -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.type.startsWith('Int')) {
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?.type.startsWith('Int')) {
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: 'Int?',
149
- attributes: relationConfig.type === 'oneToOne' ? ['@unique'] : []
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 createIdSchema(id = {}) {
303
- const defaultType = !id.generator || id.generator === 'autoincrement()' ? 'Int' : 'String';
304
- const defaultGenerator = id.type !== 'string' ? 'autoincrement()' : 'uuid()';
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: (0, common_1.capitalize)(id.type ?? defaultType),
308
- attributes: ['@id', `@default(${id.generator ?? defaultGenerator})`]
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' && scalar.maxLength && !isSqlite) {
372
- attributes.push(`@db.VarChar(${scalar.maxLength})`);
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: `Int${relationSuffix}`,
483
- attributes: relation.type === 'oneToOne' ? ['@unique'] : []
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: 'Int?',
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: 'Int?'
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) ? `[${idx.join(', ')}]` : 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appweaver/cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Appweaver - the backend framework for AI-first development (@cli)",
5
5
  "author": "Luka Matosevic",
6
6
  "license": "MIT",
@@ -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 and an
145
- optional `checkOAuth2User` callback to allow or reject OAuth2 registrations/logins (return nothing to proceed, or a
146
- string/`Error` to abort).
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