nestgen-cli 0.0.7 → 0.2.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.
Files changed (35) hide show
  1. package/dist/engine/architecture-profile.js +10 -0
  2. package/dist/engine/file-transaction.js +166 -0
  3. package/dist/engine/generated-code.js +26 -0
  4. package/dist/engine/generation-manifest.js +118 -0
  5. package/dist/engine/migration-plan.js +78 -0
  6. package/dist/engine/module-generator.js +157 -0
  7. package/dist/engine/project-config.js +42 -0
  8. package/dist/engine/project-path.js +25 -0
  9. package/dist/engine/project-preflight.js +96 -0
  10. package/dist/engine/resource-generator.js +1153 -0
  11. package/dist/engine/resource-spec.js +287 -0
  12. package/examples/store-api/README.md +76 -0
  13. package/examples/store-api/definitions/customers.resource.json +14 -0
  14. package/examples/store-api/definitions/order-items.resource.json +26 -0
  15. package/examples/store-api/definitions/orders.resource.json +22 -0
  16. package/examples/store-api/definitions/products.resource.json +19 -0
  17. package/examples/store-api/nestgen.example.json +7 -0
  18. package/nestgen.js +700 -86
  19. package/nestjs-generator/features/add_module.sh +68 -24
  20. package/nestjs-generator/features/configure_bootstrap.mjs +42 -0
  21. package/nestjs-generator/features/docker.sh +169 -36
  22. package/nestjs-generator/features/git.sh +4 -2
  23. package/nestjs-generator/features/inject_module_to_app.sh +22 -69
  24. package/nestjs-generator/features/operational_foundation.mjs +210 -0
  25. package/nestjs-generator/features/operations.sh +16 -0
  26. package/nestjs-generator/features/preflight.mjs +13 -0
  27. package/nestjs-generator/features/prisma.sh +48 -8
  28. package/nestjs-generator/features/resource_name.mjs +57 -0
  29. package/nestjs-generator/features/swagger.sh +7 -20
  30. package/nestjs-generator/features/typeorm.sh +68 -7
  31. package/nestjs-generator/features/update_app_module.mjs +146 -0
  32. package/nestjs-generator/features/utils.sh +52 -14
  33. package/nestjs-generator/generate_project.sh +46 -70
  34. package/package.json +63 -31
  35. package/readme.md +298 -120
@@ -0,0 +1,287 @@
1
+ const namePattern = /^[a-z][a-z0-9]*$/i;
2
+ const resourceNamePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/i;
3
+ const enumValuePattern = /^[A-Z][A-Z0-9_]*$/;
4
+ const basicTypes = new Set(['string', 'number', 'integer', 'boolean', 'date', 'uuid']);
5
+ function parseType(rawType, value) {
6
+ const basicType = rawType.toLowerCase();
7
+ if (basicTypes.has(basicType))
8
+ return { type: basicType };
9
+ const decimal = /^decimal\((\d+);(\d+)\)$/i.exec(rawType);
10
+ if (decimal) {
11
+ const precision = Number(decimal[1]);
12
+ const scale = Number(decimal[2]);
13
+ if (precision < 1 || precision > 1000 || scale > precision)
14
+ throw new Error(`Précision décimale invalide : ${value}. La précision doit être entre 1 et 1000, et l'échelle ne peut pas la dépasser.`);
15
+ return { type: 'decimal', precision, scale };
16
+ }
17
+ const enumeration = /^enum\(([^)]+)\)$/i.exec(rawType);
18
+ if (enumeration) {
19
+ const enumValues = enumeration[1].split('|');
20
+ if (enumValues.some((entry) => !enumValuePattern.test(entry)) || new Set(enumValues).size !== enumValues.length)
21
+ throw new Error(`Enum invalide : ${value}. Les valeurs doivent être uniques, en MAJUSCULES, et séparées par |.`);
22
+ return { type: 'enum', enumValues };
23
+ }
24
+ throw new Error(`Champ invalide : ${value}. Format attendu : nom:type, decimal(précision;échelle) ou enum(VALEUR|VALEUR), avec ? (nullable) ou ! (unique).`);
25
+ }
26
+ function parsePositiveInteger(value, label, field) {
27
+ if (!/^\d+$/.test(value) || Number(value) < 1)
28
+ throw new Error(`${label} invalide pour ${field}. Une valeur entière strictement positive est requise.`);
29
+ return Number(value);
30
+ }
31
+ function parseNumber(value, label, field) {
32
+ const parsed = Number(value);
33
+ if (!Number.isFinite(parsed))
34
+ throw new Error(`${label} invalide pour ${field}. Une valeur numérique finie est requise.`);
35
+ return parsed;
36
+ }
37
+ function parseDefaultValue(raw, field, value) {
38
+ if (field.type === 'boolean') {
39
+ if (raw !== 'true' && raw !== 'false')
40
+ throw new Error(`Valeur par défaut invalide : ${value}.`);
41
+ return raw === 'true';
42
+ }
43
+ if (field.type === 'integer') {
44
+ const parsed = parseNumber(raw, 'Valeur par défaut', value);
45
+ if (!Number.isInteger(parsed))
46
+ throw new Error(`Valeur par défaut invalide : ${value}. Un entier est requis.`);
47
+ return parsed;
48
+ }
49
+ if (field.type === 'number')
50
+ return parseNumber(raw, 'Valeur par défaut', value);
51
+ if (field.type === 'decimal') {
52
+ if (!/^-?\d+(?:\.\d+)?$/.test(raw))
53
+ throw new Error(`Valeur par défaut invalide : ${value}. Un décimal est requis.`);
54
+ const [, fraction = ''] = raw.split('.');
55
+ if (fraction.length > field.scale || raw.replace(/[-.]/g, '').length > field.precision)
56
+ throw new Error(`Valeur par défaut invalide : ${value}. Elle dépasse decimal(${field.precision};${field.scale}).`);
57
+ return raw;
58
+ }
59
+ if (field.type === 'enum') {
60
+ if (!field.enumValues.includes(raw))
61
+ throw new Error(`Valeur par défaut invalide : ${value}.`);
62
+ return raw;
63
+ }
64
+ if (field.type === 'string') {
65
+ if (!/^[\p{L}\p{N} _.-]+$/u.test(raw))
66
+ throw new Error(`Valeur par défaut invalide : ${value}.`);
67
+ return raw;
68
+ }
69
+ throw new Error(`Les valeurs par défaut ne sont pas supportées pour le type ${field.type}.`);
70
+ }
71
+ function parseConstraints(raw, field, value) {
72
+ if (!raw)
73
+ return {};
74
+ const constraints = {};
75
+ for (const entry of raw.split(';')) {
76
+ const [key, rawValue] = entry.split('=', 2);
77
+ if (!key || (rawValue === undefined && key !== 'index'))
78
+ throw new Error(`Contrainte invalide : ${value}.`);
79
+ if (key === 'index') {
80
+ if (rawValue !== undefined || constraints.indexed)
81
+ throw new Error(`Contrainte invalide : ${value}.`);
82
+ constraints.indexed = true;
83
+ continue;
84
+ }
85
+ if (!rawValue || ['length', 'min', 'max', 'default'].includes(key) === false)
86
+ throw new Error(`Contrainte invalide : ${value}.`);
87
+ if (key === 'length') {
88
+ if (field.type !== 'string' || constraints.length !== undefined)
89
+ throw new Error(`La contrainte length est réservée aux chaînes : ${value}.`);
90
+ constraints.length = parsePositiveInteger(rawValue, 'Longueur', value);
91
+ }
92
+ else if (key === 'min' || key === 'max') {
93
+ if (field.type !== 'number' && field.type !== 'integer')
94
+ throw new Error(`La contrainte ${key} est réservée aux nombres : ${value}.`);
95
+ if (constraints[key] !== undefined)
96
+ throw new Error(`Contrainte ${key} déclarée plusieurs fois : ${value}.`);
97
+ constraints[key] = parseNumber(rawValue, key === 'min' ? 'Borne minimale' : 'Borne maximale', value);
98
+ }
99
+ else {
100
+ if (constraints.defaultValue !== undefined)
101
+ throw new Error(`Valeur par défaut déclarée plusieurs fois : ${value}.`);
102
+ constraints.defaultValue = parseDefaultValue(rawValue, field, value);
103
+ }
104
+ }
105
+ if (constraints.min !== undefined && constraints.max !== undefined && constraints.min > constraints.max)
106
+ throw new Error(`Bornes incohérentes : ${value}. min ne peut pas dépasser max.`);
107
+ if (constraints.length !== undefined &&
108
+ typeof constraints.defaultValue === 'string' &&
109
+ constraints.defaultValue.length > constraints.length)
110
+ throw new Error(`Valeur par défaut trop longue : ${value}.`);
111
+ return constraints;
112
+ }
113
+ export function parseResourceFields(values) {
114
+ const names = new Set();
115
+ const fields = values.map((value) => {
116
+ const match = /^([^:]+):(.+?)(?:\{([^{}]+)\})?(\?)?(!)?$/.exec(value.trim());
117
+ if (!match)
118
+ throw new Error(`Champ invalide : ${value}. Format attendu : nom:type, avec ? (nullable) ou ! (unique).`);
119
+ const [, rawName, rawType, rawConstraints, optional, unique] = match;
120
+ if (!namePattern.test(rawName))
121
+ throw new Error(`Nom de champ invalide : ${rawName}.`);
122
+ const name = `${rawName[0].toLowerCase()}${rawName.slice(1)}`;
123
+ if (name === 'id')
124
+ throw new Error('Le champ id est géré par NestGen et ne doit pas être déclaré.');
125
+ if (names.has(name))
126
+ throw new Error(`Le champ ${name} est déclaré plusieurs fois.`);
127
+ names.add(name);
128
+ const field = {
129
+ name,
130
+ ...parseType(rawType, value),
131
+ nullable: Boolean(optional),
132
+ unique: Boolean(unique),
133
+ };
134
+ const constraints = parseConstraints(rawConstraints, field, value);
135
+ if (field.nullable && constraints.defaultValue !== undefined)
136
+ throw new Error(`Un champ nullable ne peut pas définir de valeur par défaut : ${value}.`);
137
+ return { ...field, ...constraints };
138
+ });
139
+ if (!fields.length)
140
+ throw new Error('Une ressource requiert au moins un champ métier.');
141
+ return fields;
142
+ }
143
+ export function parseResourceIndexes(values, fields, uniqueValues = []) {
144
+ const knownFields = new Set(fields.map((field) => field.name));
145
+ const indexes = [
146
+ ...fields.filter((field) => field.indexed).map((field) => ({ fields: [field.name], unique: false })),
147
+ ...values.map((value) => ({ fields: value.trim().split('+'), unique: false })),
148
+ ...uniqueValues.map((value) => ({ fields: value.trim().split('+'), unique: true })),
149
+ ];
150
+ const seen = new Set();
151
+ return indexes.map((index) => {
152
+ if (index.fields.length === 0 ||
153
+ index.fields.some((field) => !namePattern.test(field) || !knownFields.has(field)))
154
+ throw new Error(`Index invalide : ${index.fields.join('+')}. Chaque champ doit être déclaré dans la ressource.`);
155
+ if (new Set(index.fields).size !== index.fields.length)
156
+ throw new Error(`Index invalide : ${index.fields.join('+')}. Un champ est répété.`);
157
+ const key = index.fields.join('+');
158
+ if (seen.has(key))
159
+ throw new Error(`Index déclaré plusieurs fois : ${key}.`);
160
+ seen.add(key);
161
+ return index.unique ? { fields: index.fields, unique: true } : { fields: index.fields };
162
+ });
163
+ }
164
+ export function parseResourceRelations(values) {
165
+ const seenFields = new Set();
166
+ return values.map((value, index) => {
167
+ if (!value || typeof value !== 'object' || Array.isArray(value))
168
+ throw new Error(`Relation invalide à l'index ${index}. Un objet est requis.`);
169
+ const relation = value;
170
+ if (relation.type !== 'belongsTo' && relation.type !== 'manyToMany')
171
+ throw new Error(`Relation invalide à l'index ${index}. Les types belongsTo et manyToMany sont supportés.`);
172
+ if (typeof relation.target !== 'string' || !resourceNamePattern.test(relation.target))
173
+ throw new Error(`Relation invalide à l'index ${index}. target doit être un nom de ressource valide.`);
174
+ if (relation.field !== undefined && (typeof relation.field !== 'string' || !namePattern.test(relation.field)))
175
+ throw new Error(`Relation invalide à l'index ${index}. field doit être un identifiant valide.`);
176
+ if (relation.inverse !== undefined &&
177
+ (typeof relation.inverse !== 'string' || !namePattern.test(relation.inverse)))
178
+ throw new Error(`Relation invalide à l'index ${index}. inverse doit être un identifiant valide.`);
179
+ if (relation.nullable !== undefined && typeof relation.nullable !== 'boolean')
180
+ throw new Error(`Relation invalide à l'index ${index}. nullable doit être un booléen.`);
181
+ if (relation.onDelete !== undefined && relation.onDelete !== 'RESTRICT' && relation.onDelete !== 'SET NULL')
182
+ throw new Error(`Relation invalide à l'index ${index}. onDelete accepte RESTRICT ou SET NULL.`);
183
+ if (relation.type === 'manyToMany' && (relation.nullable !== undefined || relation.onDelete !== undefined))
184
+ throw new Error(`Relation invalide à l'index ${index}. manyToMany ne définit ni nullable ni onDelete.`);
185
+ const nullable = relation.nullable ?? false;
186
+ const onDelete = relation.onDelete ?? 'RESTRICT';
187
+ if (onDelete === 'SET NULL' && !nullable)
188
+ throw new Error(`Relation invalide à l'index ${index}. SET NULL requiert nullable: true.`);
189
+ const target = relation.target.toLowerCase();
190
+ const field = relation.field ? `${relation.field[0].toLowerCase()}${relation.field.slice(1)}` : undefined;
191
+ if (field && seenFields.has(field))
192
+ throw new Error(`Relation invalide à l'index ${index}. Le champ ${field} est déclaré plusieurs fois.`);
193
+ if (field)
194
+ seenFields.add(field);
195
+ return {
196
+ type: relation.type,
197
+ target,
198
+ field,
199
+ inverse: relation.inverse ? `${relation.inverse[0].toLowerCase()}${relation.inverse.slice(1)}` : undefined,
200
+ nullable,
201
+ onDelete,
202
+ };
203
+ });
204
+ }
205
+ const filterOperatorsByType = {
206
+ string: new Set(['eq', 'neq', 'contains']),
207
+ number: new Set(['eq', 'neq', 'gt', 'gte', 'lt', 'lte']),
208
+ integer: new Set(['eq', 'neq', 'gt', 'gte', 'lt', 'lte']),
209
+ decimal: new Set(['eq', 'neq', 'gt', 'gte', 'lt', 'lte']),
210
+ boolean: new Set(['eq', 'neq']),
211
+ date: new Set(['eq', 'neq', 'gt', 'gte', 'lt', 'lte']),
212
+ uuid: new Set(['eq', 'neq']),
213
+ enum: new Set(['eq', 'neq']),
214
+ };
215
+ export function parseResourceListOptions(value, fields) {
216
+ if (value === undefined)
217
+ return { cursor: false, filters: {}, sort: [], search: [] };
218
+ if (!value || typeof value !== 'object' || Array.isArray(value))
219
+ throw new Error('list doit être un objet.');
220
+ const list = value;
221
+ if (Object.keys(list).some((key) => !['cursor', 'filters', 'sort', 'search'].includes(key)))
222
+ throw new Error('list contient une propriété inconnue.');
223
+ if (list.cursor !== undefined && typeof list.cursor !== 'boolean')
224
+ throw new Error('list.cursor doit être un booléen.');
225
+ const knownFields = new Map(fields.map((field) => [field.name, field]));
226
+ const filtersRaw = list.filters ?? {};
227
+ if (!filtersRaw || typeof filtersRaw !== 'object' || Array.isArray(filtersRaw))
228
+ throw new Error('list.filters doit être un objet.');
229
+ const filters = {};
230
+ for (const [name, operators] of Object.entries(filtersRaw)) {
231
+ const field = knownFields.get(name);
232
+ if (!field)
233
+ throw new Error(`list.filters.${name} cible un champ inconnu.`);
234
+ if (!Array.isArray(operators) ||
235
+ operators.length === 0 ||
236
+ operators.some((operator) => typeof operator !== 'string'))
237
+ throw new Error(`list.filters.${name} doit contenir au moins un opérateur.`);
238
+ const normalized = operators;
239
+ if (new Set(normalized).size !== normalized.length)
240
+ throw new Error(`list.filters.${name} contient un opérateur répété.`);
241
+ if (normalized.some((operator) => !filterOperatorsByType[field.type].has(operator)))
242
+ throw new Error(`list.filters.${name} contient un opérateur incompatible avec ${field.type}.`);
243
+ filters[name] = normalized;
244
+ }
245
+ const parseFieldList = (key, allowed) => {
246
+ const values = list[key] ?? [];
247
+ if (!Array.isArray(values) || values.some((entry) => typeof entry !== 'string'))
248
+ throw new Error(`list.${key} doit être un tableau de noms de champs.`);
249
+ if (new Set(values).size !== values.length)
250
+ throw new Error(`list.${key} contient un champ répété.`);
251
+ for (const name of values) {
252
+ const field = knownFields.get(name);
253
+ if (!field || !allowed(field))
254
+ throw new Error(`list.${key}.${name} n'est pas autorisé.`);
255
+ }
256
+ return values;
257
+ };
258
+ return {
259
+ cursor: list.cursor ?? false,
260
+ filters,
261
+ sort: parseFieldList('sort', () => true),
262
+ search: parseFieldList('search', (field) => field.type === 'string'),
263
+ };
264
+ }
265
+ export function typescriptType(field) {
266
+ return field.type === 'number' || field.type === 'integer'
267
+ ? 'number'
268
+ : field.type === 'boolean'
269
+ ? 'boolean'
270
+ : field.type === 'date'
271
+ ? 'Date'
272
+ : 'string';
273
+ }
274
+ export function prismaType(field) {
275
+ const type = field.type === 'number'
276
+ ? 'Float'
277
+ : field.type === 'integer'
278
+ ? 'Int'
279
+ : field.type === 'decimal'
280
+ ? 'Decimal'
281
+ : field.type === 'boolean'
282
+ ? 'Boolean'
283
+ : field.type === 'date'
284
+ ? 'DateTime'
285
+ : 'String';
286
+ return `${type}${field.nullable ? '?' : ''}${field.unique ? ' @unique' : ''}`;
287
+ }
@@ -0,0 +1,76 @@
1
+ # Exemple Store API — NestGen CLI 0.2.1
2
+
3
+ Cette application exemple décrit une API de vente avec des clients, produits, commandes et lignes de commande. Elle est validée avec `nestgen-cli@0.2.1` et TypeORM, avec le profil d’architecture `advanced`.
4
+
5
+ Les fichiers de `definitions/` sont la source de vérité. Ils restent volontairement déclaratifs : l’application est reconstruite par le CLI au lieu de conserver une copie de fichiers générés qui pourrait diverger du produit.
6
+
7
+ ## Parcours reproductible
8
+
9
+ Créez un dossier de travail, installez la version exacte du CLI puis générez le projet Nest avec PostgreSQL et Swagger :
10
+
11
+ ```bash
12
+ mkdir nestgen-store-api && cd nestgen-store-api
13
+ npm init --yes
14
+ npm install nestgen-cli@0.2.1
15
+
16
+ ./node_modules/.bin/nestgen init store-api \
17
+ --no-interactive \
18
+ --project-path ./generated \
19
+ --package-manager npm \
20
+ --orm typeorm \
21
+ --docker \
22
+ --swagger \
23
+ --no-operations \
24
+ --no-git \
25
+ --modules ''
26
+ ```
27
+
28
+ Copiez les définitions fournies par le paquet dans le projet, puis générez-les dans l’ordre indiqué. Les relations ciblent une ressource déjà générée ; cet ordre est donc un contrat de l’exemple.
29
+
30
+ ```bash
31
+ cd generated/store-api
32
+ cp -R ../../node_modules/nestgen-cli/examples/store-api/definitions .
33
+
34
+ ../../node_modules/.bin/nestgen config init --orm typeorm --profile advanced --package-manager npm
35
+ ../../node_modules/.bin/nestgen resource --file definitions/customers.resource.json
36
+ ../../node_modules/.bin/nestgen resource --file definitions/products.resource.json
37
+ ../../node_modules/.bin/nestgen resource --file definitions/orders.resource.json
38
+ ../../node_modules/.bin/nestgen resource --file definitions/order-items.resource.json
39
+ ```
40
+
41
+ La commande de copie doit conserver les fichiers JSON inchangés. Les chemins sont relatifs au dossier initial : adaptez-les si vous utilisez un autre emplacement de travail.
42
+
43
+ ## Schéma métier et migrations
44
+
45
+ `customer.email` et `product.sku` sont uniques. Une commande référence obligatoirement un client. Une ligne de commande référence une commande et un produit, et la paire `orderId + productId` est unique : un même produit n’apparaît qu’une fois par commande. Les relations utilisent `RESTRICT`, afin d’empêcher la suppression d’un client, produit ou commande encore référencé.
46
+
47
+ Générez et appliquez une migration explicite avant de démarrer l’API :
48
+
49
+ ```bash
50
+ cp .env.example .env
51
+ docker compose up --detach postgres
52
+ npx typeorm-ts-node-commonjs migration:generate \
53
+ src/database/migrations/CreateStoreSchema \
54
+ --dataSource src/database/data-source.ts
55
+ npx typeorm-ts-node-commonjs migration:run \
56
+ --dataSource src/database/data-source.ts
57
+ npm run start:dev
58
+ ```
59
+
60
+ Vous pouvez ensuite créer un client, un produit, une commande avec son `customerId`, puis une ligne de commande avec les identifiants de commande et de produit. Swagger est disponible sur `http://localhost:3000/api` lorsque l’option a été activée durant `init`.
61
+
62
+ Pour faire évoluer le modèle, modifiez une définition, inspectez le plan et la revue de risque, puis générez la migration vous-même :
63
+
64
+ ```bash
65
+ ../../node_modules/.bin/nestgen resource \
66
+ --file definitions/customers.resource.json \
67
+ --update \
68
+ --migration-name add-customer-phone \
69
+ --dry-run
70
+ ```
71
+
72
+ Le CLI ne crée ni n’applique la migration : relisez le diff et exécutez la commande TypeORM affichée seulement après validation.
73
+
74
+ ## Validation maintenue
75
+
76
+ Depuis le dépôt NestGen, `npm run test:example` reconstruit dans un répertoire temporaire les quatre ressources via le binaire CLI. Le test vérifie les contraintes métier, les relations TypeORM, l’unicité de ligne de commande et le plan de migration. La CI l’exécute avec les tests d’intégration ; le paquet npm vérifie aussi que l’exemple est distribué.
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "name": "customer",
4
+ "route": "customers",
5
+ "table": "customers",
6
+ "orm": "typeorm",
7
+ "profile": "advanced",
8
+ "fields": ["email:string{length=254}!", "name:string{length=120}"],
9
+ "list": {
10
+ "filters": { "email": ["eq", "contains"] },
11
+ "search": ["email", "name"],
12
+ "sort": ["email", "name"]
13
+ }
14
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": 1,
3
+ "name": "order-item",
4
+ "route": "order-items",
5
+ "table": "order_items",
6
+ "orm": "typeorm",
7
+ "profile": "advanced",
8
+ "fields": ["quantity:integer{min=1}", "unitPrice:decimal(12;2)"],
9
+ "relations": [
10
+ {
11
+ "type": "belongsTo",
12
+ "target": "order",
13
+ "field": "order",
14
+ "inverse": "items",
15
+ "onDelete": "RESTRICT"
16
+ },
17
+ {
18
+ "type": "belongsTo",
19
+ "target": "product",
20
+ "field": "product",
21
+ "inverse": "orderItems",
22
+ "onDelete": "RESTRICT"
23
+ }
24
+ ],
25
+ "uniqueIndexes": ["orderId+productId"]
26
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "name": "order",
4
+ "route": "orders",
5
+ "table": "orders",
6
+ "orm": "typeorm",
7
+ "profile": "advanced",
8
+ "fields": ["reference:string{length=64}!", "status:enum(DRAFT|PLACED|CANCELLED){default=DRAFT}"],
9
+ "relations": [
10
+ {
11
+ "type": "belongsTo",
12
+ "target": "customer",
13
+ "field": "customer",
14
+ "inverse": "orders",
15
+ "onDelete": "RESTRICT"
16
+ }
17
+ ],
18
+ "list": {
19
+ "filters": { "status": ["eq"] },
20
+ "sort": ["reference", "status"]
21
+ }
22
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 1,
3
+ "name": "product",
4
+ "route": "products",
5
+ "table": "products",
6
+ "orm": "typeorm",
7
+ "profile": "advanced",
8
+ "fields": [
9
+ "sku:string{length=64}!",
10
+ "name:string{length=160}",
11
+ "unitPrice:decimal(12;2)",
12
+ "stock:integer{min=0;default=0}"
13
+ ],
14
+ "list": {
15
+ "filters": { "sku": ["eq", "contains"], "stock": ["gte", "lt"] },
16
+ "search": ["sku", "name"],
17
+ "sort": ["sku", "unitPrice", "stock"]
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 1,
3
+ "cliVersion": "0.2.1",
4
+ "orm": "typeorm",
5
+ "profile": "advanced",
6
+ "resources": ["customers", "products", "orders", "order-items"]
7
+ }