@appweaver/data 0.0.0-stage → 2.0.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/LICENSE +21 -0
- package/README.md +61 -2
- package/config.d.ts +6 -0
- package/config.js +22 -0
- package/database/create-client.d.ts +11 -0
- package/database/create-client.js +145 -0
- package/database/database-config.d.ts +14 -0
- package/database/database-config.js +40 -0
- package/database/database-error.d.ts +49 -0
- package/database/database-error.js +216 -0
- package/database/index.d.ts +3 -0
- package/database/index.js +19 -0
- package/database/prisma-database.d.ts +33 -0
- package/database/prisma-database.js +178 -0
- package/database/run-transaction.d.ts +21 -0
- package/database/run-transaction.js +15 -0
- package/export/export-config.d.ts +7 -0
- package/export/export-config.js +17 -0
- package/export/export-service.d.ts +22 -0
- package/export/export-service.js +188 -0
- package/export/index.d.ts +1 -0
- package/export/index.js +17 -0
- package/factory/create-model.d.ts +11 -0
- package/factory/create-model.js +479 -0
- package/factory/create-policy.d.ts +12 -0
- package/factory/create-policy.js +20 -0
- package/factory/create-service.d.ts +14 -0
- package/factory/create-service.js +116 -0
- package/index.d.ts +22 -0
- package/index.js +38 -0
- package/internal.d.ts +2 -0
- package/internal.js +6 -0
- package/module.d.ts +3 -0
- package/module.js +81 -0
- package/package.json +67 -3
- package/prisma/client/browser.d.ts +39 -0
- package/prisma/client/browser.js +56 -0
- package/prisma/client/client.d.ts +58 -0
- package/prisma/client/client.js +70 -0
- package/prisma/client/commonInputTypes.d.ts +345 -0
- package/prisma/client/commonInputTypes.js +11 -0
- package/prisma/client/enums.d.ts +1 -0
- package/prisma/client/enums.js +11 -0
- package/prisma/client/internal/class.d.ts +198 -0
- package/prisma/client/internal/class.js +85 -0
- package/prisma/client/internal/prismaNamespace.d.ts +1125 -0
- package/prisma/client/internal/prismaNamespace.js +204 -0
- package/prisma/client/internal/prismaNamespaceBrowser.d.ts +137 -0
- package/prisma/client/internal/prismaNamespaceBrowser.js +175 -0
- package/prisma/client/models/ApiKey.d.ts +1167 -0
- package/prisma/client/models/ApiKey.js +2 -0
- package/prisma/client/models/ConnectedAccount.d.ts +1094 -0
- package/prisma/client/models/ConnectedAccount.js +2 -0
- package/prisma/client/models/File.d.ts +1291 -0
- package/prisma/client/models/File.js +2 -0
- package/prisma/client/models/OneTimeToken.d.ts +1055 -0
- package/prisma/client/models/OneTimeToken.js +2 -0
- package/prisma/client/models/Permission.d.ts +1209 -0
- package/prisma/client/models/Permission.js +2 -0
- package/prisma/client/models/Role.d.ts +1209 -0
- package/prisma/client/models/Role.js +2 -0
- package/prisma/client/models/Seeder.d.ts +1023 -0
- package/prisma/client/models/Seeder.js +2 -0
- package/prisma/client/models.d.ts +8 -0
- package/prisma/client/models.js +2 -0
- package/resource/load-data-resources.d.ts +13 -0
- package/resource/load-data-resources.js +37 -0
- package/resource/model-schema.d.ts +34 -0
- package/resource/model-schema.js +54 -0
- package/resource/resource-config.d.ts +16 -0
- package/resource/resource-config.js +62 -0
- package/resource/resource-loader.d.ts +25 -0
- package/resource/resource-loader.js +138 -0
- package/resource/resource-service.d.ts +462 -0
- package/resource/resource-service.js +1042 -0
- package/resource/schemas/index.d.ts +3 -0
- package/resource/schemas/index.js +19 -0
- package/resource/schemas/resource-aggregate-schema.d.ts +53 -0
- package/resource/schemas/resource-aggregate-schema.js +130 -0
- package/resource/schemas/resource-filter-schema.d.ts +39 -0
- package/resource/schemas/resource-filter-schema.js +153 -0
- package/resource/schemas/resource-sort-schema.d.ts +38 -0
- package/resource/schemas/resource-sort-schema.js +113 -0
- package/resource/utils/aggregate-util.d.ts +113 -0
- package/resource/utils/aggregate-util.js +322 -0
- package/resource/utils/cursor-util.d.ts +60 -0
- package/resource/utils/cursor-util.js +116 -0
- package/resource/utils/date-util.d.ts +22 -0
- package/resource/utils/date-util.js +112 -0
- package/resource/utils/delete-util.d.ts +131 -0
- package/resource/utils/delete-util.js +385 -0
- package/resource/utils/filter-util.d.ts +24 -0
- package/resource/utils/filter-util.js +289 -0
- package/resource/utils/prune-util.d.ts +11 -0
- package/resource/utils/prune-util.js +56 -0
- package/resource/utils/relation-util.d.ts +75 -0
- package/resource/utils/relation-util.js +400 -0
- package/resource/utils/sort-util.d.ts +31 -0
- package/resource/utils/sort-util.js +235 -0
- package/resource/utils/text-search-util.d.ts +14 -0
- package/resource/utils/text-search-util.js +39 -0
- package/resources.d.ts +10 -0
- package/resources.js +26 -0
- package/security/auth-config.d.ts +23 -0
- package/security/auth-config.js +43 -0
- package/security/create-auth-resources.d.ts +28 -0
- package/security/create-auth-resources.js +163 -0
- package/security/helper.d.ts +109 -0
- package/security/helper.js +298 -0
- package/security/resources/api-key/model.d.ts +2 -0
- package/security/resources/api-key/model.js +67 -0
- package/security/resources/api-key/policy.d.ts +2 -0
- package/security/resources/api-key/policy.js +27 -0
- package/security/resources/api-key/service.d.ts +3 -0
- package/security/resources/api-key/service.js +34 -0
- package/security/resources/connected-account/model.d.ts +2 -0
- package/security/resources/connected-account/model.js +57 -0
- package/security/resources/connected-account/service.d.ts +2 -0
- package/security/resources/connected-account/service.js +7 -0
- package/security/resources/one-time-token/model.d.ts +2 -0
- package/security/resources/one-time-token/model.js +36 -0
- package/security/resources/permission/model.d.ts +2 -0
- package/security/resources/permission/model.js +16 -0
- package/security/resources/role/model.d.ts +2 -0
- package/security/resources/role/model.js +22 -0
- package/security/store/database-security-store.d.ts +5 -0
- package/security/store/database-security-store.js +54 -0
- package/seeder/create-seeder.d.ts +29 -0
- package/seeder/create-seeder.js +28 -0
- package/seeder/index.d.ts +2 -0
- package/seeder/index.js +18 -0
- package/seeder/resources/seeder/model.d.ts +2 -0
- package/seeder/resources/seeder/model.js +38 -0
- package/seeder/seeder.d.ts +29 -0
- package/seeder/seeder.js +197 -0
- package/storage/file-service.d.ts +133 -0
- package/storage/file-service.js +586 -0
- package/storage/image-config.d.ts +3 -0
- package/storage/image-config.js +11 -0
- package/storage/resources/file/model.d.ts +2 -0
- package/storage/resources/file/model.js +80 -0
- package/types/auth.d.ts +23 -0
- package/types/auth.js +2 -0
- package/types/generated.d.ts +366 -0
- package/types/generated.js +3 -0
- package/types/storage.d.ts +35 -0
- package/types/storage.js +2 -0
- package/utils/export-util.d.ts +8 -0
- package/utils/export-util.js +26 -0
- package/utils/file-util.d.ts +106 -0
- package/utils/file-util.js +254 -0
- package/utils/model-util.d.ts +36 -0
- package/utils/model-util.js +154 -0
- package/utils/policy-util.d.ts +18 -0
- package/utils/policy-util.js +28 -0
- package/utils/virtual-util.d.ts +11 -0
- package/utils/virtual-util.js +53 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildFileUrl = buildFileUrl;
|
|
4
|
+
exports.deletedResourceFileFields = deletedResourceFileFields;
|
|
5
|
+
exports.parseRange = parseRange;
|
|
6
|
+
exports.maxFileSize = maxFileSize;
|
|
7
|
+
exports.sizeInBytes = sizeInBytes;
|
|
8
|
+
exports.isValidMimeType = isValidMimeType;
|
|
9
|
+
exports.generateFileName = generateFileName;
|
|
10
|
+
exports.validateFileNamePatterns = validateFileNamePatterns;
|
|
11
|
+
exports.sanitizeFilename = sanitizeFilename;
|
|
12
|
+
exports.sanitizeFileSegment = sanitizeFileSegment;
|
|
13
|
+
exports.aggregateFiles = aggregateFiles;
|
|
14
|
+
const common_1 = require("@appweaver/common");
|
|
15
|
+
const core_1 = require("@appweaver/core");
|
|
16
|
+
/**
|
|
17
|
+
* Builds the access URL for a stored file. The `public` or `protected` path prefix is resolved from the access type
|
|
18
|
+
* configured in the file policy of the resource owning the file, and the route prefix is taken from the
|
|
19
|
+
* `STORAGE_FILES_ROUTE_PREFIX` configuration.
|
|
20
|
+
*
|
|
21
|
+
* @param {File} file - The file object containing the name and owning resource information.
|
|
22
|
+
* @return {string} The absolute URL for accessing the file.
|
|
23
|
+
*/
|
|
24
|
+
function buildFileUrl(file) {
|
|
25
|
+
const accessType = (0, core_1.injectPolicy)(file.resourceName ?? '', false)?.files?.[file.resourceField ?? '']?.accessType;
|
|
26
|
+
const pathPrefix = accessType === 'public' ? 'public' : 'protected';
|
|
27
|
+
const routePrefix = `/${core_1.storageConfig.FILES_ROUTE_PREFIX}/`.replace(/\/+/g, '/');
|
|
28
|
+
return `${common_1.appConfig.HOSTNAME}${routePrefix}${pathPrefix}/${file.name}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Splits the file fields of a deleted resource by what happens to their files. On a delete removing the resource from
|
|
32
|
+
* the database each field follows its `onResourceDeleted` option, which defaults to `'delete'`, while on a soft delete
|
|
33
|
+
* it follows its `onResourceSoftDeleted` option, which defaults to `'keep'`.
|
|
34
|
+
*
|
|
35
|
+
* @param {FilesConfig} [files] - The file fields of the deleted resource's model.
|
|
36
|
+
* @param {boolean} softDeleted - Whether the resource was soft deleted.
|
|
37
|
+
* @return {{deleted: string[], kept: string[]}} The fields whose files are removed from storage, and the fields whose
|
|
38
|
+
* files are retained, but no longer served.
|
|
39
|
+
*/
|
|
40
|
+
function deletedResourceFileFields(files = {}, softDeleted) {
|
|
41
|
+
const fields = { deleted: [], kept: [] };
|
|
42
|
+
for (const [field, fileField] of Object.entries(files)) {
|
|
43
|
+
const action = softDeleted
|
|
44
|
+
? (fileField.onResourceSoftDeleted ?? 'keep')
|
|
45
|
+
: (fileField.onResourceDeleted ?? 'delete');
|
|
46
|
+
fields[action === 'delete' ? 'deleted' : 'kept'].push(field);
|
|
47
|
+
}
|
|
48
|
+
return fields;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parses a range string and converts it into an object with start and end values.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} [range] - The range string to parse, typically in the format "bytes=start-end".
|
|
54
|
+
* @return {{start: number, end?: number}} An object containing the start value and an optional end value.
|
|
55
|
+
*/
|
|
56
|
+
function parseRange(range) {
|
|
57
|
+
if (!range) {
|
|
58
|
+
return { start: 0 };
|
|
59
|
+
}
|
|
60
|
+
const parts = range.replace(/bytes=/, '').split('-');
|
|
61
|
+
const start = parseInt(parts[0], 10);
|
|
62
|
+
const end = parts[1] ? parseInt(parts[1], 10) : undefined;
|
|
63
|
+
return { start, end };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Calculates the maximum file size allowed based on the provided file configuration.
|
|
67
|
+
*
|
|
68
|
+
* @param {FilesConfig} fileConfig - An object containing file configuration data, including maximum size for each file
|
|
69
|
+
* type.
|
|
70
|
+
* @return {number} The maximum size in bytes allowed for any file based on the given configuration.
|
|
71
|
+
*/
|
|
72
|
+
function maxFileSize(fileConfig) {
|
|
73
|
+
const sizes = Object.values(fileConfig)
|
|
74
|
+
.map((conf) => sizeInBytes(conf.maxSize))
|
|
75
|
+
.filter((v) => v > 0);
|
|
76
|
+
if (!sizes.length) {
|
|
77
|
+
return (0, common_1.textToBytes)(common_1.config.str('SERVER_BODY_MAX_SIZE', '100M'));
|
|
78
|
+
}
|
|
79
|
+
return Math.max(...sizes);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Converts a given size represented as a string or number into its equivalent size in bytes.
|
|
83
|
+
*
|
|
84
|
+
* @param {string | number} [size] - The size to be converted, either as a string or a number.
|
|
85
|
+
* @return {number} The size in bytes. Returns 0 if the input is invalid or not provided.
|
|
86
|
+
*/
|
|
87
|
+
function sizeInBytes(size) {
|
|
88
|
+
return (0, common_1.isString)(size) ? (0, common_1.textToBytes)(size) : 0;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Validates if the provided MIME type matches a given pattern or regular expression.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} mimeType - The MIME type to validate.
|
|
94
|
+
* @param {string|RegExp} [mimeTypeExp] - An optional pattern or regular expression to match the MIME type against.
|
|
95
|
+
* If not provided, the method will always return true.
|
|
96
|
+
* @return {boolean} True if the MIME type matches the specified pattern or regular expression, false otherwise.
|
|
97
|
+
*/
|
|
98
|
+
function isValidMimeType(mimeType, mimeTypeExp) {
|
|
99
|
+
if (!mimeTypeExp) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
else if (mimeTypeExp instanceof RegExp) {
|
|
103
|
+
return mimeTypeExp.test(mimeType);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const regexPattern = mimeTypeExp
|
|
107
|
+
.replace(/[.+?^$]/g, '\\$&')
|
|
108
|
+
.replace(/\*/g, '.*');
|
|
109
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
110
|
+
return regex.test(mimeType);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Generates a file name based on a given name, an optional pattern, and a set of variables.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} name The original file name including its extension.
|
|
117
|
+
* @param {string} [pattern] An optional pattern to customize the generated file name. If not provided, a default
|
|
118
|
+
* pattern will be used.
|
|
119
|
+
* @param {Record<string, any>} [variables={}] Additional variables to replace in the pattern. These can include custom
|
|
120
|
+
* placeholders to dynamically generate the file name.
|
|
121
|
+
* @return {string} The generated, sanitized file name based on the provided or default pattern.
|
|
122
|
+
*/
|
|
123
|
+
function generateFileName(name, pattern, variables = {}) {
|
|
124
|
+
const nameParts = name.split('.');
|
|
125
|
+
let extension = '';
|
|
126
|
+
let nameWithoutExtension = name;
|
|
127
|
+
if (nameParts.length > 1) {
|
|
128
|
+
extension = nameParts.pop();
|
|
129
|
+
nameWithoutExtension = nameParts.join('.');
|
|
130
|
+
}
|
|
131
|
+
const defaultPattern = core_1.storageConfig.NAME_PATTERN ?? '{name}-{hash}.{extension}';
|
|
132
|
+
// Only the pattern itself is trusted to contain directory separators, every substituted value is reduced to a
|
|
133
|
+
// single path segment so that an uploaded file name cannot introduce a sub-path or a traversal.
|
|
134
|
+
let fileName = (0, common_1.replacePatternVariables)(pattern ?? defaultPattern, {
|
|
135
|
+
...sanitizeVariables(variables),
|
|
136
|
+
name: sanitizeFileSegment(nameWithoutExtension),
|
|
137
|
+
extension: sanitizeFileSegment(extension)
|
|
138
|
+
});
|
|
139
|
+
fileName = sanitizeFilename(fileName);
|
|
140
|
+
fileName = fileName.endsWith('.')
|
|
141
|
+
? fileName.substring(0, fileName.length - 1)
|
|
142
|
+
: fileName;
|
|
143
|
+
return ((0, common_1.normalizeStoragePath)(fileName) ||
|
|
144
|
+
sanitizeFileSegment(name) ||
|
|
145
|
+
(0, common_1.generateToken)('bytes', 32));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Validates that no configured file name pattern writes into a path reserved by `STORAGE_RESERVED_PATHS`. Both the
|
|
149
|
+
* global `STORAGE_NAME_PATTERN` and the `namePattern` of every file field of the provided models are checked. Patterns
|
|
150
|
+
* defined as a factory function cannot be resolved before an upload, so they are validated at runtime instead.
|
|
151
|
+
*
|
|
152
|
+
* @param {Record<string, ResourceModel>} models - The loaded resource models, keyed by model name.
|
|
153
|
+
* @throws {Error} Throws an error naming the model, the field, and the reserved path when a pattern is reserved.
|
|
154
|
+
*/
|
|
155
|
+
function validateFileNamePatterns(models) {
|
|
156
|
+
const reservedPaths = core_1.storageConfig.RESERVED_PATHS;
|
|
157
|
+
const globalReservedPath = (0, common_1.findReservedStoragePath)(core_1.storageConfig.NAME_PATTERN, reservedPaths);
|
|
158
|
+
if (globalReservedPath !== null) {
|
|
159
|
+
throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `Configured STORAGE_NAME_PATTERN '${core_1.storageConfig.NAME_PATTERN}' is placed under the reserved ` +
|
|
160
|
+
`storage path '${globalReservedPath}'`);
|
|
161
|
+
}
|
|
162
|
+
for (const model of Object.values(models)) {
|
|
163
|
+
for (const [field, fileConfig] of Object.entries(model.config.files ?? {})) {
|
|
164
|
+
if (!(0, common_1.isString)(fileConfig.namePattern)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const reservedPath = (0, common_1.findReservedStoragePath)(fileConfig.namePattern, reservedPaths);
|
|
168
|
+
if (reservedPath !== null) {
|
|
169
|
+
throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `File name pattern '${fileConfig.namePattern}' of the '${model.name}.${field}' field is placed ` +
|
|
170
|
+
`under the reserved storage path '${reservedPath}'`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Sanitizes a given filename by removing invalid characters that are not allowed in file systems. Path separators are
|
|
177
|
+
* preserved, since a file name may be a path relative to the storage root.
|
|
178
|
+
*
|
|
179
|
+
* @param fileName The original filename to sanitize.
|
|
180
|
+
* @return A sanitized filename with invalid characters removed.
|
|
181
|
+
*/
|
|
182
|
+
function sanitizeFilename(fileName) {
|
|
183
|
+
const invalidFilenameChars = new Set([
|
|
184
|
+
'\\',
|
|
185
|
+
':',
|
|
186
|
+
'*',
|
|
187
|
+
'?',
|
|
188
|
+
'"',
|
|
189
|
+
'<',
|
|
190
|
+
'>',
|
|
191
|
+
'|'
|
|
192
|
+
]);
|
|
193
|
+
return [...fileName]
|
|
194
|
+
.filter((char) => {
|
|
195
|
+
const code = char.charCodeAt(0);
|
|
196
|
+
// Strip control characters (including NUL) and characters invalid in file names.
|
|
197
|
+
return code > 31 && code !== 127 && !invalidFilenameChars.has(char);
|
|
198
|
+
})
|
|
199
|
+
.join('');
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Sanitizes an untrusted value into a single file path segment. In addition to the characters removed by
|
|
203
|
+
* {@link sanitizeFilename}, all path separators are stripped and leading dots are removed, so that the value can never
|
|
204
|
+
* introduce a subdirectory, a `..` traversal or a hidden file.
|
|
205
|
+
*
|
|
206
|
+
* @param value The untrusted value to sanitize.
|
|
207
|
+
* @return A sanitized single path segment, or an empty string when nothing usable remains.
|
|
208
|
+
*/
|
|
209
|
+
function sanitizeFileSegment(value) {
|
|
210
|
+
if (!(0, common_1.isString)(value)) {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
// Trimming must precede the leading dot removal, otherwise a padded value such as ' .. ' keeps its dots.
|
|
214
|
+
return sanitizeFilename(value)
|
|
215
|
+
.replace(/[/\\]+/g, '')
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/^\.+/, '')
|
|
218
|
+
.trim();
|
|
219
|
+
}
|
|
220
|
+
function sanitizeVariables(variables) {
|
|
221
|
+
return Object.fromEntries(Object.entries(variables).map(([key, value]) => [
|
|
222
|
+
key,
|
|
223
|
+
(0, common_1.isString)(value) ? sanitizeFileSegment(value) : value
|
|
224
|
+
]));
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Aggregates the provided files into a structured object based on the given configuration.
|
|
228
|
+
*
|
|
229
|
+
* @param {File[]} files - An array of file objects to be processed and aggregated.
|
|
230
|
+
* @param {FilesConfig} filesConfig - An object that defines the configuration for each file field, including whether
|
|
231
|
+
* the field supports multiple files.
|
|
232
|
+
* @return {Record<string, File | File[] | null>} A record where keys correspond to the configured fields, and values
|
|
233
|
+
* are either a single file, an array of files, or null if no files match the field.
|
|
234
|
+
*/
|
|
235
|
+
function aggregateFiles(files, filesConfig) {
|
|
236
|
+
const aggregated = Object.entries(filesConfig)
|
|
237
|
+
.map(([field, value]) => ({ [field]: value.array ? [] : null }))
|
|
238
|
+
.reduce((acc, obj) => Object.assign(acc, obj), {});
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const resourceField = file.resourceField;
|
|
241
|
+
if (resourceField) {
|
|
242
|
+
if (filesConfig[resourceField].array) {
|
|
243
|
+
if (!aggregated[resourceField]) {
|
|
244
|
+
aggregated[resourceField] = [];
|
|
245
|
+
}
|
|
246
|
+
aggregated[resourceField].push(file);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
aggregated[resourceField] = file;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return aggregated;
|
|
254
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ResourceModel } from '@appweaver/common';
|
|
2
|
+
/**
|
|
3
|
+
* Checks whether the model with the given name soft deletes its records.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} [resourceName] - The name of the model to check.
|
|
6
|
+
* @return {boolean} True if the model is loaded and has `softDelete` enabled.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSoftDeleteModel(resourceName?: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Builds the condition matching only the records of a model that are not soft
|
|
11
|
+
* deleted, for the queries reading its records outside the resource service.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} resourceName - The name of the model the condition is built for.
|
|
14
|
+
* @return {Object} The `deletedAt: null` condition for a soft deleted model, or
|
|
15
|
+
* an empty object otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function liveRecordFilter(resourceName: string): Record<string, any>;
|
|
18
|
+
/**
|
|
19
|
+
* Validates that every relation cascading on delete from a model with
|
|
20
|
+
* `softDelete` enabled belongs to a model that soft deletes its records too.
|
|
21
|
+
*
|
|
22
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
23
|
+
* @throws {Error} When a soft deleted model cascades into a model without soft delete.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateSoftDeleteCascades(models: Record<string, ResourceModel>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Validates that the default value of every scalar and virtual field satisfies
|
|
28
|
+
* the constraints declared on that same field. A default outside its own
|
|
29
|
+
* constraints produces records that do not match the output schema of their
|
|
30
|
+
* model, which only surfaces once the model is nested in a nullable relation,
|
|
31
|
+
* where the response serializer validates it before picking a schema branch.
|
|
32
|
+
*
|
|
33
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
34
|
+
* @throws {Error} When any default value violates the constraints of its field.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateScalarDefaults(models: Record<string, ResourceModel>): void;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSoftDeleteModel = isSoftDeleteModel;
|
|
4
|
+
exports.liveRecordFilter = liveRecordFilter;
|
|
5
|
+
exports.validateSoftDeleteCascades = validateSoftDeleteCascades;
|
|
6
|
+
exports.validateScalarDefaults = validateScalarDefaults;
|
|
7
|
+
const common_1 = require("@appweaver/common");
|
|
8
|
+
const core_1 = require("@appweaver/core");
|
|
9
|
+
/**
|
|
10
|
+
* Checks whether the model with the given name soft deletes its records.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} [resourceName] - The name of the model to check.
|
|
13
|
+
* @return {boolean} True if the model is loaded and has `softDelete` enabled.
|
|
14
|
+
*/
|
|
15
|
+
function isSoftDeleteModel(resourceName) {
|
|
16
|
+
if (!resourceName) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const model = (0, core_1.injectModel)((0, common_1.capitalize)(resourceName), false);
|
|
20
|
+
return (0, common_1.hasSoftDelete)(model?.config);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Builds the condition matching only the records of a model that are not soft
|
|
24
|
+
* deleted, for the queries reading its records outside the resource service.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} resourceName - The name of the model the condition is built for.
|
|
27
|
+
* @return {Object} The `deletedAt: null` condition for a soft deleted model, or
|
|
28
|
+
* an empty object otherwise.
|
|
29
|
+
*/
|
|
30
|
+
function liveRecordFilter(resourceName) {
|
|
31
|
+
return isSoftDeleteModel(resourceName) ? { deletedAt: null } : {};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validates that every relation cascading on delete from a model with
|
|
35
|
+
* `softDelete` enabled belongs to a model that soft deletes its records too.
|
|
36
|
+
*
|
|
37
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
38
|
+
* @throws {Error} When a soft deleted model cascades into a model without soft delete.
|
|
39
|
+
*/
|
|
40
|
+
function validateSoftDeleteCascades(models) {
|
|
41
|
+
const errors = (0, common_1.softDeleteCascadeErrors)(models);
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `Invalid resource model soft delete cascades:\n${errors
|
|
44
|
+
.map((error) => ` - ${error}`)
|
|
45
|
+
.join('\n')}`, { errors });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validates that the default value of every scalar and virtual field satisfies
|
|
50
|
+
* the constraints declared on that same field. A default outside its own
|
|
51
|
+
* constraints produces records that do not match the output schema of their
|
|
52
|
+
* model, which only surfaces once the model is nested in a nullable relation,
|
|
53
|
+
* where the response serializer validates it before picking a schema branch.
|
|
54
|
+
*
|
|
55
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
56
|
+
* @throws {Error} When any default value violates the constraints of its field.
|
|
57
|
+
*/
|
|
58
|
+
function validateScalarDefaults(models) {
|
|
59
|
+
const errors = [];
|
|
60
|
+
for (const model of Object.values(models)) {
|
|
61
|
+
const fields = {
|
|
62
|
+
...(model.config.scalars ?? {}),
|
|
63
|
+
...(model.config.virtual ?? {})
|
|
64
|
+
};
|
|
65
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
66
|
+
for (const error of defaultValueErrors(field)) {
|
|
67
|
+
errors.push(`'${model.name}.${name}' ${error}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (errors.length > 0) {
|
|
72
|
+
throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `Invalid resource model default values:\n${errors
|
|
73
|
+
.map((error) => ` - ${error}`)
|
|
74
|
+
.join('\n')}`, { errors });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function defaultValueErrors(field) {
|
|
78
|
+
if (field.default === undefined) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
// An array field defaults to a list of values, each validated on its own
|
|
82
|
+
const values = field.array
|
|
83
|
+
? (0, common_1.isArray)(field.default)
|
|
84
|
+
? field.default
|
|
85
|
+
: [field.default]
|
|
86
|
+
: [field.default];
|
|
87
|
+
return values.flatMap((value) => defaultErrors(field, value));
|
|
88
|
+
}
|
|
89
|
+
function defaultErrors(field, value) {
|
|
90
|
+
const errors = [];
|
|
91
|
+
const invalidType = (expected) => `has a default value of ${JSON.stringify(value)}, which is not ${expected}`;
|
|
92
|
+
const violates = (constraint, limit) => `has a default value of ${JSON.stringify(value)}, which violates its ` +
|
|
93
|
+
`own ${constraint} of ${JSON.stringify(limit)}`;
|
|
94
|
+
switch (field.type) {
|
|
95
|
+
case 'string':
|
|
96
|
+
if (!(0, common_1.isString)(value)) {
|
|
97
|
+
return [invalidType('a string')];
|
|
98
|
+
}
|
|
99
|
+
if (field.minLength !== undefined && value.length < field.minLength) {
|
|
100
|
+
errors.push(violates('minLength', field.minLength));
|
|
101
|
+
}
|
|
102
|
+
if (field.maxLength !== undefined && value.length > field.maxLength) {
|
|
103
|
+
errors.push(violates('maxLength', field.maxLength));
|
|
104
|
+
}
|
|
105
|
+
if (field.pattern !== undefined &&
|
|
106
|
+
!matchesPattern(value, field.pattern)) {
|
|
107
|
+
errors.push(violates('pattern', field.pattern));
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case 'int':
|
|
111
|
+
case 'bigInt':
|
|
112
|
+
case 'float':
|
|
113
|
+
if (!(0, common_1.isNumber)(value)) {
|
|
114
|
+
return [invalidType('a number')];
|
|
115
|
+
}
|
|
116
|
+
if (field.type !== 'float' && !Number.isInteger(value)) {
|
|
117
|
+
return [invalidType('an integer')];
|
|
118
|
+
}
|
|
119
|
+
if (field.minimum !== undefined && value < field.minimum) {
|
|
120
|
+
errors.push(violates('minimum', field.minimum));
|
|
121
|
+
}
|
|
122
|
+
if (field.maximum !== undefined && value > field.maximum) {
|
|
123
|
+
errors.push(violates('maximum', field.maximum));
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case 'boolean':
|
|
127
|
+
if (!(0, common_1.isBoolean)(value)) {
|
|
128
|
+
errors.push(invalidType('a boolean'));
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case 'dateTime':
|
|
132
|
+
if (!(value instanceof Date) &&
|
|
133
|
+
(!(0, common_1.isString)(value) || Number.isNaN(Date.parse(value)))) {
|
|
134
|
+
errors.push(invalidType('a valid date'));
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
case 'enum':
|
|
138
|
+
if (!(field.values ?? []).includes(value)) {
|
|
139
|
+
errors.push(violates('allowed values', field.values ?? []));
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
return errors;
|
|
144
|
+
}
|
|
145
|
+
/** Tests a value against a field pattern, skipping patterns that are not valid
|
|
146
|
+
* regular expressions since they cannot be evaluated. */
|
|
147
|
+
function matchesPattern(value, pattern) {
|
|
148
|
+
try {
|
|
149
|
+
return new RegExp(pattern).test(value);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the given function with internal access, where the resource and file services skip the policies: the access
|
|
3
|
+
* checks, the read and write restrictions, and the file access checks. Everything else, like the service hooks,
|
|
4
|
+
* validation, events, and cache invalidation, still runs. Meant for the framework's own flows and trusted application
|
|
5
|
+
* code, such as jobs and seeders, that act on behalf of the system rather than the requesting user.
|
|
6
|
+
*
|
|
7
|
+
* The internal access covers every call made while the function runs, including the ones made by the service hooks.
|
|
8
|
+
*
|
|
9
|
+
* @param {Function} fn - The function to run with internal access.
|
|
10
|
+
* @return {Object} The value returned by the function.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withoutPolicies<T>(fn: () => T): T;
|
|
13
|
+
/**
|
|
14
|
+
* Checks whether the current call runs with internal access, started by {@link withoutPolicies}.
|
|
15
|
+
*
|
|
16
|
+
* @return {boolean} True if the policies are skipped for the current call, otherwise false.
|
|
17
|
+
*/
|
|
18
|
+
export declare function arePoliciesSkipped(): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withoutPolicies = withoutPolicies;
|
|
4
|
+
exports.arePoliciesSkipped = arePoliciesSkipped;
|
|
5
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
6
|
+
const internalAccess = new node_async_hooks_1.AsyncLocalStorage();
|
|
7
|
+
/**
|
|
8
|
+
* Runs the given function with internal access, where the resource and file services skip the policies: the access
|
|
9
|
+
* checks, the read and write restrictions, and the file access checks. Everything else, like the service hooks,
|
|
10
|
+
* validation, events, and cache invalidation, still runs. Meant for the framework's own flows and trusted application
|
|
11
|
+
* code, such as jobs and seeders, that act on behalf of the system rather than the requesting user.
|
|
12
|
+
*
|
|
13
|
+
* The internal access covers every call made while the function runs, including the ones made by the service hooks.
|
|
14
|
+
*
|
|
15
|
+
* @param {Function} fn - The function to run with internal access.
|
|
16
|
+
* @return {Object} The value returned by the function.
|
|
17
|
+
*/
|
|
18
|
+
function withoutPolicies(fn) {
|
|
19
|
+
return internalAccess.run(true, fn);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Checks whether the current call runs with internal access, started by {@link withoutPolicies}.
|
|
23
|
+
*
|
|
24
|
+
* @return {boolean} True if the policies are skipped for the current call, otherwise false.
|
|
25
|
+
*/
|
|
26
|
+
function arePoliciesSkipped() {
|
|
27
|
+
return internalAccess.getStore() === true;
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects virtual field values onto a resource object based on its model definition. Virtual fields with an output
|
|
3
|
+
* value function are evaluated against the resource, constant output values are assigned directly, and remaining
|
|
4
|
+
* required virtual fields receive their default value. Nested relation and file objects are projected recursively
|
|
5
|
+
* using their own model definitions.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} resource - The resource object to project virtual fields onto.
|
|
8
|
+
* @param {string} resourceName - The name of the resource model describing the object.
|
|
9
|
+
* @return {Object} A copy of the resource object with all virtual field values set.
|
|
10
|
+
*/
|
|
11
|
+
export declare function projectVirtualFields<T>(resource: T, resourceName: string): T;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectVirtualFields = projectVirtualFields;
|
|
4
|
+
const common_1 = require("@appweaver/common");
|
|
5
|
+
const core_1 = require("@appweaver/core");
|
|
6
|
+
/**
|
|
7
|
+
* Projects virtual field values onto a resource object based on its model definition. Virtual fields with an output
|
|
8
|
+
* value function are evaluated against the resource, constant output values are assigned directly, and remaining
|
|
9
|
+
* required virtual fields receive their default value. Nested relation and file objects are projected recursively
|
|
10
|
+
* using their own model definitions.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} resource - The resource object to project virtual fields onto.
|
|
13
|
+
* @param {string} resourceName - The name of the resource model describing the object.
|
|
14
|
+
* @return {Object} A copy of the resource object with all virtual field values set.
|
|
15
|
+
*/
|
|
16
|
+
function projectVirtualFields(resource, resourceName) {
|
|
17
|
+
if (!resource) {
|
|
18
|
+
return resource;
|
|
19
|
+
}
|
|
20
|
+
const projectedVirtual = { ...resource };
|
|
21
|
+
const resourceModel = (0, core_1.injectModel)(resourceName, false);
|
|
22
|
+
const relationsModel = resourceModel?.relationsModel;
|
|
23
|
+
const filesModel = resourceModel?.filesModel;
|
|
24
|
+
// Set output or default value for virtual fields
|
|
25
|
+
for (const [fieldName, virtual] of Object.entries(resourceModel?.config?.virtual ?? {})) {
|
|
26
|
+
const outputValue = virtual.output?.value;
|
|
27
|
+
if ((0, common_1.isFunction)(outputValue)) {
|
|
28
|
+
projectedVirtual[fieldName] = outputValue(projectedVirtual);
|
|
29
|
+
}
|
|
30
|
+
else if (outputValue) {
|
|
31
|
+
projectedVirtual[fieldName] = outputValue;
|
|
32
|
+
}
|
|
33
|
+
else if (virtual.required !== false) {
|
|
34
|
+
projectedVirtual[fieldName] =
|
|
35
|
+
virtual.default ?? (0, common_1.defaultScalarValue)(virtual);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Recursively project virtual fields for nested relational objects
|
|
39
|
+
for (const key in projectedVirtual) {
|
|
40
|
+
const value = projectedVirtual[key];
|
|
41
|
+
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
42
|
+
const fileSchema = (0, common_1.extractSchemaProperties)(filesModel, key);
|
|
43
|
+
if ((0, common_1.isPlainObject)(value) || ((0, common_1.isArray)(value) && (0, common_1.isPlainObject)(value[0]))) {
|
|
44
|
+
const nestedResourceName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
|
|
45
|
+
if (nestedResourceName) {
|
|
46
|
+
projectedVirtual[key] = (0, common_1.isArray)(value)
|
|
47
|
+
? value.map((item) => projectVirtualFields(item, nestedResourceName))
|
|
48
|
+
: projectVirtualFields(value, nestedResourceName);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return projectedVirtual;
|
|
53
|
+
}
|