@appweaver/data 0.0.0-stage → 2.0.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/config.d.ts +6 -0
  4. package/config.js +22 -0
  5. package/database/create-client.d.ts +11 -0
  6. package/database/create-client.js +145 -0
  7. package/database/database-config.d.ts +14 -0
  8. package/database/database-config.js +40 -0
  9. package/database/database-error.d.ts +49 -0
  10. package/database/database-error.js +216 -0
  11. package/database/index.d.ts +3 -0
  12. package/database/index.js +19 -0
  13. package/database/prisma-database.d.ts +33 -0
  14. package/database/prisma-database.js +178 -0
  15. package/database/run-transaction.d.ts +21 -0
  16. package/database/run-transaction.js +15 -0
  17. package/export/export-config.d.ts +7 -0
  18. package/export/export-config.js +17 -0
  19. package/export/export-service.d.ts +22 -0
  20. package/export/export-service.js +188 -0
  21. package/export/index.d.ts +1 -0
  22. package/export/index.js +17 -0
  23. package/factory/create-model.d.ts +11 -0
  24. package/factory/create-model.js +479 -0
  25. package/factory/create-policy.d.ts +12 -0
  26. package/factory/create-policy.js +20 -0
  27. package/factory/create-service.d.ts +14 -0
  28. package/factory/create-service.js +116 -0
  29. package/index.d.ts +22 -0
  30. package/index.js +38 -0
  31. package/internal.d.ts +2 -0
  32. package/internal.js +6 -0
  33. package/module.d.ts +3 -0
  34. package/module.js +81 -0
  35. package/package.json +67 -3
  36. package/prisma/client/browser.d.ts +39 -0
  37. package/prisma/client/browser.js +56 -0
  38. package/prisma/client/client.d.ts +58 -0
  39. package/prisma/client/client.js +70 -0
  40. package/prisma/client/commonInputTypes.d.ts +345 -0
  41. package/prisma/client/commonInputTypes.js +11 -0
  42. package/prisma/client/enums.d.ts +1 -0
  43. package/prisma/client/enums.js +11 -0
  44. package/prisma/client/internal/class.d.ts +198 -0
  45. package/prisma/client/internal/class.js +85 -0
  46. package/prisma/client/internal/prismaNamespace.d.ts +1125 -0
  47. package/prisma/client/internal/prismaNamespace.js +204 -0
  48. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +137 -0
  49. package/prisma/client/internal/prismaNamespaceBrowser.js +175 -0
  50. package/prisma/client/models/ApiKey.d.ts +1167 -0
  51. package/prisma/client/models/ApiKey.js +2 -0
  52. package/prisma/client/models/ConnectedAccount.d.ts +1094 -0
  53. package/prisma/client/models/ConnectedAccount.js +2 -0
  54. package/prisma/client/models/File.d.ts +1291 -0
  55. package/prisma/client/models/File.js +2 -0
  56. package/prisma/client/models/OneTimeToken.d.ts +1055 -0
  57. package/prisma/client/models/OneTimeToken.js +2 -0
  58. package/prisma/client/models/Permission.d.ts +1209 -0
  59. package/prisma/client/models/Permission.js +2 -0
  60. package/prisma/client/models/Role.d.ts +1209 -0
  61. package/prisma/client/models/Role.js +2 -0
  62. package/prisma/client/models/Seeder.d.ts +1023 -0
  63. package/prisma/client/models/Seeder.js +2 -0
  64. package/prisma/client/models.d.ts +8 -0
  65. package/prisma/client/models.js +2 -0
  66. package/resource/load-data-resources.d.ts +13 -0
  67. package/resource/load-data-resources.js +37 -0
  68. package/resource/model-schema.d.ts +34 -0
  69. package/resource/model-schema.js +54 -0
  70. package/resource/resource-config.d.ts +16 -0
  71. package/resource/resource-config.js +62 -0
  72. package/resource/resource-loader.d.ts +25 -0
  73. package/resource/resource-loader.js +138 -0
  74. package/resource/resource-service.d.ts +462 -0
  75. package/resource/resource-service.js +1042 -0
  76. package/resource/schemas/index.d.ts +3 -0
  77. package/resource/schemas/index.js +19 -0
  78. package/resource/schemas/resource-aggregate-schema.d.ts +53 -0
  79. package/resource/schemas/resource-aggregate-schema.js +130 -0
  80. package/resource/schemas/resource-filter-schema.d.ts +39 -0
  81. package/resource/schemas/resource-filter-schema.js +153 -0
  82. package/resource/schemas/resource-sort-schema.d.ts +38 -0
  83. package/resource/schemas/resource-sort-schema.js +113 -0
  84. package/resource/utils/aggregate-util.d.ts +113 -0
  85. package/resource/utils/aggregate-util.js +322 -0
  86. package/resource/utils/cursor-util.d.ts +60 -0
  87. package/resource/utils/cursor-util.js +116 -0
  88. package/resource/utils/date-util.d.ts +22 -0
  89. package/resource/utils/date-util.js +112 -0
  90. package/resource/utils/delete-util.d.ts +131 -0
  91. package/resource/utils/delete-util.js +385 -0
  92. package/resource/utils/filter-util.d.ts +24 -0
  93. package/resource/utils/filter-util.js +289 -0
  94. package/resource/utils/prune-util.d.ts +11 -0
  95. package/resource/utils/prune-util.js +56 -0
  96. package/resource/utils/relation-util.d.ts +75 -0
  97. package/resource/utils/relation-util.js +400 -0
  98. package/resource/utils/sort-util.d.ts +31 -0
  99. package/resource/utils/sort-util.js +235 -0
  100. package/resource/utils/text-search-util.d.ts +14 -0
  101. package/resource/utils/text-search-util.js +39 -0
  102. package/resources.d.ts +10 -0
  103. package/resources.js +26 -0
  104. package/security/auth-config.d.ts +23 -0
  105. package/security/auth-config.js +43 -0
  106. package/security/create-auth-resources.d.ts +28 -0
  107. package/security/create-auth-resources.js +163 -0
  108. package/security/helper.d.ts +109 -0
  109. package/security/helper.js +298 -0
  110. package/security/resources/api-key/model.d.ts +2 -0
  111. package/security/resources/api-key/model.js +67 -0
  112. package/security/resources/api-key/policy.d.ts +2 -0
  113. package/security/resources/api-key/policy.js +27 -0
  114. package/security/resources/api-key/service.d.ts +3 -0
  115. package/security/resources/api-key/service.js +34 -0
  116. package/security/resources/connected-account/model.d.ts +2 -0
  117. package/security/resources/connected-account/model.js +57 -0
  118. package/security/resources/connected-account/service.d.ts +2 -0
  119. package/security/resources/connected-account/service.js +7 -0
  120. package/security/resources/one-time-token/model.d.ts +2 -0
  121. package/security/resources/one-time-token/model.js +36 -0
  122. package/security/resources/permission/model.d.ts +2 -0
  123. package/security/resources/permission/model.js +16 -0
  124. package/security/resources/role/model.d.ts +2 -0
  125. package/security/resources/role/model.js +22 -0
  126. package/security/store/database-security-store.d.ts +5 -0
  127. package/security/store/database-security-store.js +54 -0
  128. package/seeder/create-seeder.d.ts +29 -0
  129. package/seeder/create-seeder.js +28 -0
  130. package/seeder/index.d.ts +2 -0
  131. package/seeder/index.js +18 -0
  132. package/seeder/resources/seeder/model.d.ts +2 -0
  133. package/seeder/resources/seeder/model.js +38 -0
  134. package/seeder/seeder.d.ts +29 -0
  135. package/seeder/seeder.js +197 -0
  136. package/storage/file-service.d.ts +133 -0
  137. package/storage/file-service.js +586 -0
  138. package/storage/image-config.d.ts +3 -0
  139. package/storage/image-config.js +11 -0
  140. package/storage/resources/file/model.d.ts +2 -0
  141. package/storage/resources/file/model.js +80 -0
  142. package/types/auth.d.ts +23 -0
  143. package/types/auth.js +2 -0
  144. package/types/generated.d.ts +366 -0
  145. package/types/generated.js +3 -0
  146. package/types/storage.d.ts +35 -0
  147. package/types/storage.js +2 -0
  148. package/utils/export-util.d.ts +8 -0
  149. package/utils/export-util.js +26 -0
  150. package/utils/file-util.d.ts +106 -0
  151. package/utils/file-util.js +254 -0
  152. package/utils/model-util.d.ts +36 -0
  153. package/utils/model-util.js +154 -0
  154. package/utils/policy-util.d.ts +18 -0
  155. package/utils/policy-util.js +28 -0
  156. package/utils/virtual-util.d.ts +11 -0
  157. package/utils/virtual-util.js +53 -0
@@ -0,0 +1,479 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createModel = createModel;
4
+ const typebox_1 = require("@sinclair/typebox");
5
+ const common_1 = require("@appweaver/common");
6
+ const internal_1 = require("@appweaver/core/internal");
7
+ const core_1 = require("@appweaver/core");
8
+ const model_schema_1 = require("../resource/model-schema");
9
+ /**
10
+ * Creates a resource model from its configuration and defines it in the application context. The names of the
11
+ * declared fields are inferred from the configuration, so the `read`, `create`, and `update` options only accept
12
+ * existing fields.
13
+ *
14
+ * @param {ResourceModelConfig} config - The model configuration.
15
+ * @param {boolean} [override=false] - Whether to replace a model already defined under the same name.
16
+ * @return {ResourceModel} The created resource model.
17
+ */
18
+ function createModel(modelConfig, override = false) {
19
+ const config = normalizeRelations(modelConfig);
20
+ const resourceModel = buildModel(config);
21
+ common_1.logger.debug({ modelName: resourceModel.name }, 'Created resource model');
22
+ (0, core_1.define)(resourceModel, undefined, override ? 'override' : undefined);
23
+ // The relation inputs and audit fields reference the primary key of another
24
+ // model, which may not be loaded yet. Loaders call this hook once every model
25
+ // is registered, so those references resolve against their final id type.
26
+ resourceModel[common_1.RESOURCE_MODEL_RELINK] = () => {
27
+ Object.assign(resourceModel, buildModel(config));
28
+ };
29
+ return resourceModel;
30
+ }
31
+ /**
32
+ * Replaces every `manyToOne` relation with the `oneToMany` relation it stands for, owning the foreign key, so the
33
+ * rest of the framework handles a single form.
34
+ *
35
+ * @throws {Error} If a `manyToOne` relation is declared with `owner: false`.
36
+ */
37
+ function normalizeRelations(config) {
38
+ if (!config.relations) {
39
+ return config;
40
+ }
41
+ const relations = {};
42
+ for (const [field, relation] of Object.entries(config.relations)) {
43
+ if (relation.type !== 'manyToOne') {
44
+ relations[field] = relation;
45
+ continue;
46
+ }
47
+ if (relation.owner === false) {
48
+ throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `Relation '${config.name}.${field}' of type 'manyToOne' holds the foreign key, so it cannot set 'owner: false'`);
49
+ }
50
+ relations[field] = { ...relation, type: 'oneToMany', owner: true };
51
+ }
52
+ return { ...config, relations };
53
+ }
54
+ function buildModel(config) {
55
+ const name = (0, common_1.capitalize)(config.name);
56
+ const idSchema = (0, model_schema_1.idSchema)(config?.id);
57
+ const auditSchema = buildAuditSchema(config?.audit, (0, common_1.hasSoftDelete)(config));
58
+ const scalarsSchema = buildScalarsSchema(config?.scalars);
59
+ const virtualSchema = buildScalarsSchema(config?.virtual);
60
+ const filesSchema = buildFilesSchema(config?.files);
61
+ const relationsSchema = buildRelationsSchema(config?.relations);
62
+ const readModel = typebox_1.Type.Composite([
63
+ idSchema,
64
+ scalarsSchema,
65
+ virtualSchema,
66
+ relationsSchema,
67
+ filesSchema,
68
+ auditSchema
69
+ ], {
70
+ $id: name
71
+ });
72
+ const virtualModel = typebox_1.Type.Composite([virtualSchema], {
73
+ $id: `${name}Virtual`
74
+ });
75
+ const relationsModel = typebox_1.Type.Composite([relationsSchema], {
76
+ $id: `${name}Relations`
77
+ });
78
+ const filesModel = typebox_1.Type.Composite([filesSchema], { $id: `${name}Files` });
79
+ const baseReadModel = omitOrPickScalars(typebox_1.Type.Composite([idSchema, scalarsSchema, virtualSchema, auditSchema]), config.read);
80
+ const createModel = omitOrPickScalars(resolveDefaultScalars(typebox_1.Type.Composite([scalarsSchema, virtualSchema]), config), config.create);
81
+ const updateModel = omitOrPickScalars(resolveDefaultScalars(typebox_1.Type.Partial(typebox_1.Type.Composite([scalarsSchema, virtualSchema])), config), config.update);
82
+ // The fields of an update, its scalars required the same way as on a
83
+ // create, since a replacement carries the whole stored state of the
84
+ // resource. The virtual fields are no part of it, so they stay optional
85
+ const replaceModel = omitOrPickScalars(resolveDefaultScalars(typebox_1.Type.Composite([scalarsSchema, typebox_1.Type.Partial(virtualSchema)]), config), config.update);
86
+ const { readOneModel, readManyModel } = buildOutputModels(baseReadModel, relationsModel, filesModel, config);
87
+ const readOneNullableModel = buildNullableOutputModel(name);
88
+ const { createOneModel, updateOneModel, replaceOneModel, relationCreateModel, relationUpdateModel } = buildInputModels(createModel, updateModel, replaceModel, relationsModel, idSchema, config);
89
+ const { fileUploadModel, fileDeleteModel } = buildFileInputModels(config);
90
+ const resourceModel = {
91
+ name,
92
+ config: config,
93
+ idModel: idSchema,
94
+ readModel,
95
+ createModel,
96
+ updateModel,
97
+ relationsModel,
98
+ filesModel,
99
+ virtualModel,
100
+ readOneModel,
101
+ readManyModel,
102
+ readOneNullableModel,
103
+ createOneModel,
104
+ updateOneModel,
105
+ replaceOneModel,
106
+ relationCreateModel,
107
+ relationUpdateModel,
108
+ fileUploadModel,
109
+ fileDeleteModel
110
+ };
111
+ for (const value of Object.values(resourceModel)) {
112
+ if ((0, common_1.isPlainObject)(value)) {
113
+ value[common_1.RESOURCE_NAME] = name;
114
+ }
115
+ }
116
+ resourceModel[common_1.RESOURCE_NAME] = name;
117
+ resourceModel[common_1.RESOURCE_TYPE] = common_1.RESOURCE_MODEL_TYPE;
118
+ return resourceModel;
119
+ }
120
+ function referencedIdField(modelName) {
121
+ if (!modelName) {
122
+ return undefined;
123
+ }
124
+ return internal_1.context.resource.models.get((0, common_1.capitalize)(modelName))?.config?.id;
125
+ }
126
+ function authModel() {
127
+ for (const model of internal_1.context.resource.models.values()) {
128
+ if ((0, common_1.isResourceAuthModel)(model)) {
129
+ return model;
130
+ }
131
+ }
132
+ }
133
+ function authIdField() {
134
+ return authModel()?.config?.id;
135
+ }
136
+ function buildAuditSchema(audit = {}, softDelete = false) {
137
+ const defaultAudit = {
138
+ updatedAt: true,
139
+ createdAt: true,
140
+ createdById: true
141
+ };
142
+ const mergedAudit = { ...defaultAudit, ...audit };
143
+ const fields = [];
144
+ for (const [name, included] of Object.entries(mergedAudit)) {
145
+ if (included) {
146
+ fields.push(name);
147
+ }
148
+ }
149
+ const auditSchema = typebox_1.Type.Pick((0, model_schema_1.auditSchema)(authIdField()), fields);
150
+ if (!softDelete) {
151
+ return auditSchema;
152
+ }
153
+ // The soft delete columns are typed on the model, but hidden like any hidden
154
+ // field, so no request or response carries them
155
+ return typebox_1.Type.Object({
156
+ ...auditSchema.properties,
157
+ deletedAt: (0, common_1.Nullable)((0, common_1.StringDate)({ hidden: true })),
158
+ ...(authModel()
159
+ ? {
160
+ deletedById: (0, common_1.Nullable)({
161
+ ...(0, model_schema_1.idValueSchema)(authIdField()),
162
+ hidden: true
163
+ })
164
+ }
165
+ : {})
166
+ });
167
+ }
168
+ function buildScalarsSchema(fields = {}) {
169
+ const properties = {};
170
+ for (const [name, field] of Object.entries(fields)) {
171
+ properties[name] = buildScalarSchema(field);
172
+ }
173
+ return typebox_1.Type.Object(properties);
174
+ }
175
+ function buildScalarSchema(field) {
176
+ let fieldType;
177
+ switch (field.type) {
178
+ case 'string':
179
+ fieldType = typebox_1.Type.String((0, common_1.pickProperties)(field, [
180
+ 'minLength',
181
+ 'maxLength',
182
+ 'format',
183
+ 'pattern',
184
+ 'hidden',
185
+ 'example'
186
+ ]));
187
+ break;
188
+ case 'int':
189
+ case 'bigInt':
190
+ fieldType = typebox_1.Type.Integer((0, common_1.pickProperties)(field, ['minimum', 'maximum', 'hidden', 'example']));
191
+ break;
192
+ case 'float':
193
+ fieldType = typebox_1.Type.Number((0, common_1.pickProperties)(field, ['minimum', 'maximum', 'hidden', 'example']));
194
+ break;
195
+ case 'boolean':
196
+ fieldType = typebox_1.Type.Boolean((0, common_1.pickProperties)(field, ['hidden', 'example']));
197
+ break;
198
+ case 'dateTime':
199
+ fieldType = (0, common_1.StringDate)((0, common_1.pickProperties)(field, ['format', 'hidden', 'example']));
200
+ break;
201
+ case 'json':
202
+ fieldType = (0, common_1.AnyJson)((0, common_1.pickProperties)(field, ['hidden', 'example']));
203
+ break;
204
+ case 'enum':
205
+ fieldType = (0, common_1.StringEnum)(field.values ?? [], (0, common_1.pickProperties)(field, ['hidden', 'example']));
206
+ break;
207
+ }
208
+ if (field.array === true) {
209
+ fieldType = typebox_1.Type.Array(fieldType);
210
+ }
211
+ if (field.required === false) {
212
+ fieldType = (0, common_1.Nullable)(fieldType);
213
+ }
214
+ return fieldType;
215
+ }
216
+ function omitOrPickScalars(type, operationConfig) {
217
+ let restrictedType = type;
218
+ if (operationConfig?.pick) {
219
+ restrictedType = typebox_1.Type.Pick(type, operationConfig.pick);
220
+ }
221
+ else if (operationConfig?.omit) {
222
+ restrictedType = typebox_1.Type.Omit(type, operationConfig.omit);
223
+ }
224
+ return typebox_1.Type.Composite([restrictedType]);
225
+ }
226
+ function resolveDefaultScalars(type, config) {
227
+ for (const name of Object.keys(type.properties)) {
228
+ const field = type.properties[name];
229
+ if (config?.scalars?.[name]?.default !== undefined ||
230
+ config?.virtual?.[name]?.default !== undefined) {
231
+ type.properties[name] = typebox_1.Type.Optional(field);
232
+ }
233
+ }
234
+ return type;
235
+ }
236
+ function buildFilesSchema(files = {}) {
237
+ const properties = {};
238
+ for (const [name, file] of Object.entries(files)) {
239
+ properties[name] = buildFileSchema(file);
240
+ }
241
+ return typebox_1.Type.Object(properties);
242
+ }
243
+ function buildFileSchema(file) {
244
+ const fileSchema = typebox_1.Type.Ref('FileSingle');
245
+ return file.array ? typebox_1.Type.Array(fileSchema) : (0, common_1.Nullable)(fileSchema);
246
+ }
247
+ function buildRelationsSchema(relations = {}) {
248
+ const properties = {};
249
+ for (const [name, relation] of Object.entries(relations)) {
250
+ properties[name] = buildRelationSchema(relation);
251
+ }
252
+ return typebox_1.Type.Object(properties);
253
+ }
254
+ function buildRelationSchema(relation) {
255
+ const modelName = (0, common_1.capitalize)(relation.model);
256
+ const modelRefName = `${modelName}Single`;
257
+ let relationType = typebox_1.Type.Ref(modelRefName);
258
+ relationType = (0, common_1.isRelationArray)(relation)
259
+ ? typebox_1.Type.Array(relationType, (0, common_1.pickProperties)(relation, ['minItems']))
260
+ : relationType;
261
+ return relation.required === false
262
+ ? typebox_1.Type.Optional(relationType)
263
+ : relationType;
264
+ }
265
+ function buildOutputModels(readModel, relationsModel, filesModel, config) {
266
+ const virtualConfig = config.virtual;
267
+ const relationsConfig = config.relations;
268
+ const filesConfig = config.files;
269
+ const adjustedReadModel = removeHiddenFields(readModel);
270
+ const readOneModel = typebox_1.Type.Composite([
271
+ resolveOutputVirtualFields(adjustedReadModel, virtualConfig, 'single'),
272
+ relationOutputProperties(relationsModel, relationsConfig, 'single'),
273
+ relationOutputProperties(filesModel, filesConfig, 'single')
274
+ ], { $id: `${config.name}Single` });
275
+ const readManyModel = typebox_1.Type.Composite([
276
+ resolveOutputVirtualFields(adjustedReadModel, virtualConfig, 'multiple'),
277
+ relationOutputProperties(relationsModel, relationsConfig, 'multiple'),
278
+ relationOutputProperties(filesModel, filesConfig, 'multiple')
279
+ ], { $id: `${config.name}Multiple` });
280
+ return { readOneModel, readManyModel };
281
+ }
282
+ function buildNullableOutputModel(name) {
283
+ // Builds the nullable variant of the single read model, registered under its own
284
+ // name so a nullable relation can reference it instead of inlining the union.
285
+ // The response serializer cannot compile an inline `anyOf` union that cycles
286
+ // back to a model it is already writing, since it expands every union branch
287
+ // into a fresh schema. A union behind a name is expanded once and reused, which
288
+ // keeps a self-referencing relation (a category and its parent) serializable.
289
+ return typebox_1.Type.Union([typebox_1.Type.Ref(`${name}Single`), typebox_1.Type.Null()], {
290
+ $id: `${name}SingleNullable`
291
+ });
292
+ }
293
+ function buildInputModels(createModel, updateModel, replaceModel, relationsModel, idSchema, config) {
294
+ const virtualConfig = config.virtual;
295
+ const relationsConfig = config.relations;
296
+ const adjustedCreateModel = resolveInputVirtualFields(removeHiddenFields(createModel), virtualConfig, 'create');
297
+ const adjustedUpdateModel = resolveInputVirtualFields(removeHiddenFields(updateModel), virtualConfig, 'update');
298
+ const adjustedReplaceModel = resolveInputVirtualFields(removeHiddenFields(replaceModel), virtualConfig, 'update');
299
+ // Data models accepted when this model is written inline through another
300
+ // model's relation input: created without an id, or updated by its id.
301
+ // Relations and files are excluded, so nested writes stay limited to the
302
+ // model's own columns.
303
+ const relationCreateModel = typebox_1.Type.Composite([adjustedCreateModel], {
304
+ $id: `${config.name}RelationCreate`
305
+ });
306
+ const relationUpdateModel = typebox_1.Type.Composite([idSchema, adjustedUpdateModel], {
307
+ $id: `${config.name}RelationUpdate`
308
+ });
309
+ if (Object.keys(relationsConfig ?? {}).length === 0) {
310
+ return {
311
+ createOneModel: adjustedCreateModel,
312
+ updateOneModel: adjustedUpdateModel,
313
+ replaceOneModel: adjustedReplaceModel,
314
+ relationCreateModel,
315
+ relationUpdateModel
316
+ };
317
+ }
318
+ const relationCreateInputs = relationInputProperties(relationsModel, relationsConfig, 'create');
319
+ const createOneModel = typebox_1.Type.Composite([adjustedCreateModel, relationCreateInputs], {
320
+ $id: `${config.name}Create`
321
+ });
322
+ const relationUpdateInputs = relationInputProperties(relationsModel, relationsConfig, 'update');
323
+ const updateOneModel = typebox_1.Type.Composite([adjustedUpdateModel, typebox_1.Type.Partial(relationUpdateInputs)], { $id: `${config.name}Update` });
324
+ // The required relations are required, while the optional ones a
325
+ // replacement leaves out stay untouched
326
+ const replaceOneModel = typebox_1.Type.Composite([adjustedReplaceModel, relationUpdateInputs], { $id: `${config.name}Replace` });
327
+ return {
328
+ createOneModel,
329
+ updateOneModel,
330
+ replaceOneModel,
331
+ relationCreateModel,
332
+ relationUpdateModel
333
+ };
334
+ }
335
+ function buildFileInputModels(config) {
336
+ const fileConfig = config.files ?? {};
337
+ const FileUpload = typebox_1.Type.Unsafe({
338
+ type: 'string',
339
+ format: 'binary',
340
+ [typebox_1.Kind]: 'String'
341
+ });
342
+ const FileDelete = typebox_1.Type.String({ example: 'image_123.png' });
343
+ const fileUploadModel = typebox_1.Type.Object(Object.fromEntries(Object.entries(fileConfig).map(([key, conf]) => [
344
+ key,
345
+ typebox_1.Type.Optional(conf.array ? typebox_1.Type.Array(FileUpload) : FileUpload)
346
+ ])), { $id: `${config.name}FileUpload` });
347
+ const fileDeleteModel = typebox_1.Type.Object(Object.fromEntries(Object.entries(fileConfig).map(([key, conf]) => [
348
+ key,
349
+ typebox_1.Type.Optional(conf.array ? typebox_1.Type.Array(FileDelete) : FileDelete)
350
+ ])), { $id: `${config.name}FileDelete` });
351
+ return { fileUploadModel, fileDeleteModel };
352
+ }
353
+ function relationInputProperties(object, relationConfig, inputType) {
354
+ const relationInputType = (key) => {
355
+ const { type, ...options } = object.properties[key];
356
+ const config = relationConfig?.[key];
357
+ if (config && shouldSkipInputField(config.input?.type, inputType)) {
358
+ return undefined;
359
+ }
360
+ // Inline updates carry the related record id, so only parent update
361
+ // requests accept them, since the database updates relations only within
362
+ // an update action
363
+ const acceptsUpdate = config?.input?.allowUpdate && inputType === 'update';
364
+ // Existing records are connected by an id object or a bare id value, both
365
+ // typed after the primary key of the related model. An item with an id is
366
+ // an inline update when the relation accepts them (a lone id still
367
+ // connects), and an item without one is an inline create
368
+ const relatedIdField = referencedIdField(config?.model);
369
+ const itemSchemas = [
370
+ acceptsUpdate
371
+ ? typebox_1.Type.Ref(`${config.model}RelationUpdate`)
372
+ : (0, model_schema_1.idSchema)(relatedIdField),
373
+ ...(config?.input?.allowCreate
374
+ ? [typebox_1.Type.Ref(`${config.model}RelationCreate`)]
375
+ : []),
376
+ (0, model_schema_1.idValueSchema)(relatedIdField)
377
+ ];
378
+ const isArrayType = type === 'array';
379
+ const isOptional = config?.required === false;
380
+ let inputSchema;
381
+ if (isArrayType) {
382
+ // One array of union items, so connect, create, and update inputs mix
383
+ const arraySchema = typebox_1.Type.Array(typebox_1.Type.Union(itemSchemas), options);
384
+ inputSchema = isOptional ? (0, common_1.Nullable)(arraySchema) : arraySchema;
385
+ }
386
+ else {
387
+ inputSchema = typebox_1.Type.Union(itemSchemas.map((itemSchema) => isOptional ? (0, common_1.Nullable)(itemSchema) : itemSchema));
388
+ }
389
+ return isOptional ? typebox_1.Type.Optional(inputSchema) : inputSchema;
390
+ };
391
+ return typebox_1.Type.Object({
392
+ ...Object.keys(object.properties).reduce((acc, key) => {
393
+ const type = relationInputType(key);
394
+ if (type) {
395
+ acc[key] = type;
396
+ }
397
+ return acc;
398
+ }, {})
399
+ });
400
+ }
401
+ function relationOutputProperties(object, relationConfig, outputType) {
402
+ const relationOutputType = (key) => {
403
+ let schema = object.properties[key];
404
+ const config = relationConfig?.[key];
405
+ if (config) {
406
+ if (shouldSkipOutputField(config.output?.type, outputType)) {
407
+ return undefined;
408
+ }
409
+ if (config.required === false && schema.type !== 'array') {
410
+ schema = nullableOutputSchema(schema);
411
+ }
412
+ }
413
+ return schema;
414
+ };
415
+ const relationCountType = (key) => {
416
+ const config = relationConfig?.[key];
417
+ if (!config?.output?.count) {
418
+ return undefined;
419
+ }
420
+ return typebox_1.Type.Integer({ minimum: 0 });
421
+ };
422
+ // Relation, file, and count fields are optional in the output models: their
423
+ // presence depends on the inclusion depth of the query. The same schema
424
+ // describes a record nested in another model's output, without relations.
425
+ return typebox_1.Type.Object({
426
+ ...Object.keys(object.properties).reduce((acc, key) => {
427
+ const type = relationOutputType(key);
428
+ if (type) {
429
+ acc[key] = typebox_1.Type.Optional(type);
430
+ }
431
+ const countType = relationCountType(key);
432
+ if (countType) {
433
+ acc[(0, common_1.countFieldName)(key)] = typebox_1.Type.Optional(countType);
434
+ }
435
+ return acc;
436
+ }, {})
437
+ });
438
+ }
439
+ function nullableOutputSchema(schema) {
440
+ const modelRef = schema.$ref;
441
+ return modelRef ? typebox_1.Type.Ref(`${modelRef}Nullable`) : (0, common_1.Nullable)(schema);
442
+ }
443
+ function removeHiddenFields(schema) {
444
+ const properties = {};
445
+ for (const [name, field] of Object.entries(schema.properties)) {
446
+ if (!field.hidden) {
447
+ properties[name] = field;
448
+ }
449
+ }
450
+ return typebox_1.Type.Object(properties);
451
+ }
452
+ function resolveInputVirtualFields(schema, virtualConfig, inputType) {
453
+ const excludeFields = [];
454
+ for (const [name, virtual] of Object.entries(virtualConfig ?? {})) {
455
+ if (shouldSkipInputField(virtual.input?.type, inputType)) {
456
+ excludeFields.push(name);
457
+ }
458
+ }
459
+ return typebox_1.Type.Omit(schema, excludeFields);
460
+ }
461
+ function resolveOutputVirtualFields(schema, virtualConfig, outputType) {
462
+ const excludeFields = [];
463
+ for (const [name, virtual] of Object.entries(virtualConfig ?? {})) {
464
+ if (shouldSkipOutputField(virtual.output?.type, outputType)) {
465
+ excludeFields.push(name);
466
+ }
467
+ }
468
+ return typebox_1.Type.Omit(schema, excludeFields);
469
+ }
470
+ function shouldSkipInputField(fieldInputType, methodInputType) {
471
+ return (fieldInputType === 'none' ||
472
+ (methodInputType === 'create' && fieldInputType === 'update') ||
473
+ (methodInputType === 'update' && fieldInputType === 'create'));
474
+ }
475
+ function shouldSkipOutputField(fieldOutputType, methodOutputType) {
476
+ return (fieldOutputType === 'none' ||
477
+ (methodOutputType === 'single' && fieldOutputType === 'multiple') ||
478
+ (methodOutputType === 'multiple' && fieldOutputType === 'single'));
479
+ }
@@ -0,0 +1,12 @@
1
+ import { ModelName, ResourcePolicyConfig, ResourceRecord } from '@appweaver/common';
2
+ /**
3
+ * Creates the policy of a model, checking the access to its records and restricting the reads and writes. Once the
4
+ * types are generated, the records the callbacks receive are typed by the model name.
5
+ *
6
+ * @param {ResourcePolicyConfig} config - The policy configuration with the model name and the policy callbacks.
7
+ * @param {boolean} [override=false] - Whether to replace a policy already defined for the model.
8
+ * @return {ResourcePolicyConfig} The created policy, defined in the application context.
9
+ */
10
+ export declare function createPolicy<N extends ModelName, T = ResourceRecord<N>>(config: ResourcePolicyConfig<T> & {
11
+ modelName: N;
12
+ }, override?: boolean): ResourcePolicyConfig<T>;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPolicy = createPolicy;
4
+ const common_1 = require("@appweaver/common");
5
+ const core_1 = require("@appweaver/core");
6
+ /**
7
+ * Creates the policy of a model, checking the access to its records and restricting the reads and writes. Once the
8
+ * types are generated, the records the callbacks receive are typed by the model name.
9
+ *
10
+ * @param {ResourcePolicyConfig} config - The policy configuration with the model name and the policy callbacks.
11
+ * @param {boolean} [override=false] - Whether to replace a policy already defined for the model.
12
+ * @return {ResourcePolicyConfig} The created policy, defined in the application context.
13
+ */
14
+ function createPolicy(config, override = false) {
15
+ config[common_1.RESOURCE_NAME] = (0, common_1.capitalize)(config.modelName);
16
+ config[common_1.RESOURCE_TYPE] = common_1.RESOURCE_POLICY_TYPE;
17
+ common_1.logger.debug({ modelName: config.modelName }, 'Created resource policy');
18
+ (0, core_1.define)(config, undefined, override ? 'override' : undefined);
19
+ return config;
20
+ }
@@ -0,0 +1,14 @@
1
+ import { Ctor, ModelName, QueryFilter, RegistryType, ResourceServiceConfig } from '@appweaver/common';
2
+ import { ResourceService } from '../resource/resource-service';
3
+ /**
4
+ * Creates the resource service of a model, running the configured hooks around its operations and applying the
5
+ * policy of the model. The update hooks run around both an update and a replacement. Once the types are generated,
6
+ * the model, create, and update types of the hooks are inferred from the model name.
7
+ *
8
+ * @param {ResourceServiceConfig} config - The service configuration with the model name, hooks, and text search.
9
+ * @param {boolean} [override=false] - Whether to replace a service already defined for the model.
10
+ * @return {Ctor<ResourceService>} The created service class, defined in the application context.
11
+ */
12
+ export declare function createService<N extends ModelName, T = RegistryType<N, 'model'>, C = RegistryType<N, 'create'>, U = RegistryType<N, 'update'>, R = RegistryType<N, 'replace', C>>(config: ResourceServiceConfig<T, C, U> & {
13
+ modelName: N;
14
+ }, override?: boolean): Ctor<ResourceService<T, T, C, U, QueryFilter<T>, R>>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createService = createService;
4
+ const common_1 = require("@appweaver/common");
5
+ const core_1 = require("@appweaver/core");
6
+ const resource_service_1 = require("../resource/resource-service");
7
+ const text_search_util_1 = require("../resource/utils/text-search-util");
8
+ /**
9
+ * Creates the resource service of a model, running the configured hooks around its operations and applying the
10
+ * policy of the model. The update hooks run around both an update and a replacement. Once the types are generated,
11
+ * the model, create, and update types of the hooks are inferred from the model name.
12
+ *
13
+ * @param {ResourceServiceConfig} config - The service configuration with the model name, hooks, and text search.
14
+ * @param {boolean} [override=false] - Whether to replace a service already defined for the model.
15
+ * @return {Ctor<ResourceService>} The created service class, defined in the application context.
16
+ */
17
+ function createService(config, override = false) {
18
+ const name = (0, common_1.capitalize)(config.modelName);
19
+ class Service extends resource_service_1.ResourceService {
20
+ [common_1.CONFIG] = config;
21
+ [common_1.RESOURCE_NAME] = name;
22
+ [common_1.RESOURCE_TYPE] = common_1.RESOURCE_SERVICE_TYPE;
23
+ constructor() {
24
+ super(name);
25
+ }
26
+ async find(id) {
27
+ await config.beforeFind?.(id);
28
+ const result = await super.find(id);
29
+ await config.afterFind?.(result);
30
+ return result;
31
+ }
32
+ async query(options = {}) {
33
+ await config.beforeQuery?.(options);
34
+ const result = await super.query(options);
35
+ await config.afterQuery?.(result);
36
+ return result;
37
+ }
38
+ async aggregate(options) {
39
+ await config.beforeAggregate?.(options);
40
+ const result = await super.aggregate(options);
41
+ // The hook is declared once for the model, so it takes the response of
42
+ // every selection, of which this one holds a subset of the fields
43
+ await config.afterAggregate?.(result);
44
+ return result;
45
+ }
46
+ async create(data) {
47
+ await config.beforeCreate?.(data);
48
+ const result = await super.create(data);
49
+ await config.afterCreate?.(result);
50
+ return result;
51
+ }
52
+ async update(id, data) {
53
+ await config.beforeUpdate?.(id, data);
54
+ const { previous, current } = await this.updateWithPrevious(id, data);
55
+ await config.afterUpdate?.(current, previous);
56
+ return current;
57
+ }
58
+ async replace(id, data) {
59
+ // The hooks receive the whole replacement, the reset fields included
60
+ const replacement = this.replacementData(data);
61
+ await config.beforeUpdate?.(id, replacement);
62
+ const { previous, current } = await this.updateWithPrevious(id, replacement);
63
+ await config.afterUpdate?.(current, previous);
64
+ return current;
65
+ }
66
+ async delete(id) {
67
+ await config.beforeDelete?.(id);
68
+ const result = await super.delete(id);
69
+ await config.afterDelete?.(result);
70
+ return result;
71
+ }
72
+ textSearchQuery(searchText) {
73
+ if ((0, common_1.isFunction)(config.textSearch)) {
74
+ return config.textSearch(searchText);
75
+ }
76
+ if ((0, common_1.isPlainObject)(config.textSearch)) {
77
+ return (0, text_search_util_1.bindTextSearch)(config.textSearch, searchText);
78
+ }
79
+ return super.textSearchQuery(searchText);
80
+ }
81
+ async readRestrictions(action, data) {
82
+ const policy = (0, core_1.injectPolicy)(name, false);
83
+ if (policy?.readRestrictions) {
84
+ const user = (0, core_1.currentAuthUser)() ?? null;
85
+ return (await policy.readRestrictions(user, data, action)) ?? {};
86
+ }
87
+ return super.readRestrictions(action, data);
88
+ }
89
+ async writeRestrictions(action, data) {
90
+ const policy = (0, core_1.injectPolicy)(name, false);
91
+ if (policy?.writeRestrictions) {
92
+ const user = (0, core_1.currentAuthUser)() ?? null;
93
+ return (await policy.writeRestrictions(user, data, action)) ?? {};
94
+ }
95
+ return super.writeRestrictions(action, data);
96
+ }
97
+ async checkAccess(action, resource) {
98
+ const policy = (0, core_1.injectPolicy)(name, false);
99
+ if (policy?.checkAccess) {
100
+ const user = (0, core_1.currentAuthUser)() ?? null;
101
+ return policy.checkAccess(user, resource, action);
102
+ }
103
+ return super.checkAccess(action, resource);
104
+ }
105
+ }
106
+ Object.defineProperty(Service, 'name', {
107
+ value: `${name}Service`,
108
+ configurable: true
109
+ });
110
+ Service[common_1.CONFIG] = config;
111
+ Service[common_1.RESOURCE_NAME] = name;
112
+ Service[common_1.RESOURCE_TYPE] = common_1.RESOURCE_SERVICE_TYPE;
113
+ common_1.logger.debug({ modelName: config.modelName }, 'Created resource service');
114
+ (0, core_1.define)(Service, undefined, override ? 'override' : undefined);
115
+ return Service;
116
+ }
package/index.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export * from './config';
2
+ export * from './database';
3
+ export * from './export/export-service';
4
+ export * from './factory/create-model';
5
+ export * from './factory/create-policy';
6
+ export * from './factory/create-service';
7
+ export * from './resource/model-schema';
8
+ export * from './resource/resource-loader';
9
+ export * from './resource/resource-service';
10
+ export * from './resource/schemas';
11
+ export * from './security/create-auth-resources';
12
+ export * from './security/helper';
13
+ export * from './seeder';
14
+ export * from './storage/file-service';
15
+ export * from './types/auth';
16
+ export * from './types/generated';
17
+ export * from './types/storage';
18
+ export * from './utils/export-util';
19
+ export * from './utils/file-util';
20
+ export * from './utils/model-util';
21
+ export * from './utils/policy-util';
22
+ export * from './utils/virtual-util';