@appweaver/core 1.0.24 → 1.1.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/prisma/client/internal/class.js +4 -4
  3. package/prisma/client/internal/prismaNamespace.d.ts +1 -0
  4. package/prisma/client/internal/prismaNamespace.js +1 -0
  5. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +1 -0
  6. package/prisma/client/internal/prismaNamespaceBrowser.js +1 -0
  7. package/prisma/client/models/File.d.ts +29 -1
  8. package/resource/resource-service.d.ts +0 -1
  9. package/resource/resource-service.js +2 -39
  10. package/security/auth-service.d.ts +16 -1
  11. package/security/auth-service.js +28 -0
  12. package/security/create-auth-resources.d.ts +2 -1
  13. package/security/oauth2/create-oauth2-plugin.d.ts +2 -7
  14. package/security/oauth2/create-oauth2-plugin.js +45 -1
  15. package/security/oauth2/oauth2-custom.d.ts +1 -0
  16. package/security/oauth2/oauth2-custom.js +2 -1
  17. package/security/oauth2/oauth2-facebook.js +3 -2
  18. package/security/oauth2/oauth2-google.js +2 -1
  19. package/seeder/seeder.d.ts +1 -1
  20. package/seeder/seeder.js +5 -5
  21. package/server/index.d.ts +1 -0
  22. package/server/index.js +1 -0
  23. package/server/register-route.js +13 -0
  24. package/server/virtual-projection.d.ts +26 -0
  25. package/server/virtual-projection.js +231 -0
  26. package/storage/file-service.js +21 -7
  27. package/storage/resources/file/model.js +7 -8
  28. package/types/auth.d.ts +17 -1
  29. package/types/generated.d.ts +5 -0
  30. package/utils/file-util.d.ts +9 -0
  31. package/utils/file-util.js +16 -0
  32. package/utils/index.d.ts +1 -0
  33. package/utils/index.js +1 -0
  34. package/utils/virtual-util.d.ts +11 -0
  35. package/utils/virtual-util.js +53 -0
@@ -13,7 +13,7 @@ exports.oauth2Facebook = (0, create_oauth2_plugin_1.createOAuth2Plugin)(common_1
13
13
  });
14
14
  async function fetchFacebookUser(accessToken) {
15
15
  const params = new URLSearchParams();
16
- params.append('fields', 'id,name,email');
16
+ params.append('fields', 'id,name,email,picture.width(512)');
17
17
  params.append('access_token', accessToken);
18
18
  const resp = await fetch(`${common_1.config.SECURITY_OAUTH2_FACEBOOK_USER_INFO_URL}?${params}`, { method: 'GET' });
19
19
  if (!resp.ok) {
@@ -25,6 +25,7 @@ async function fetchFacebookUser(accessToken) {
25
25
  id: data.id,
26
26
  email: data.email,
27
27
  firstName,
28
- lastName
28
+ lastName,
29
+ avatarUrl: data.picture?.data?.url
29
30
  };
30
31
  }
@@ -23,6 +23,7 @@ async function fetchGoogleUser(accessToken) {
23
23
  id: data.id,
24
24
  email: data.email,
25
25
  firstName: data.given_name,
26
- lastName: data.family_name
26
+ lastName: data.family_name,
27
+ avatarUrl: data.picture
27
28
  };
28
29
  }
@@ -10,7 +10,7 @@ export declare class Seeder extends LifecycleManager {
10
10
  /**
11
11
  * Executes the seeders by discovering, validating, and running seeder files.
12
12
  * Logs the seeding process, including warnings for checksum mismatches
13
- * and files that do not exist but are present in database table.
13
+ * and files that do not exist but are present in the database table.
14
14
  *
15
15
  * @return Resolves when all seeders have been processed. Logs relevant
16
16
  * information during execution.
package/seeder/seeder.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Seeder = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const promises_1 = __importDefault(require("node:fs/promises"));
9
10
  const common_1 = require("@appweaver/common");
10
11
  const lifecycle_manager_1 = require("../app/lifecycle-manager");
@@ -27,7 +28,7 @@ class Seeder extends lifecycle_manager_1.LifecycleManager {
27
28
  /**
28
29
  * Executes the seeders by discovering, validating, and running seeder files.
29
30
  * Logs the seeding process, including warnings for checksum mismatches
30
- * and files that do not exist but are present in database table.
31
+ * and files that do not exist but are present in the database table.
31
32
  *
32
33
  * @return Resolves when all seeders have been processed. Logs relevant
33
34
  * information during execution.
@@ -133,11 +134,11 @@ class Seeder extends lifecycle_manager_1.LifecycleManager {
133
134
  }
134
135
  }
135
136
  }
136
- const seederHash = await this.seederHash(seederFile);
137
+ const checksum = await this.seederHash(seederFile);
137
138
  // Insert seeder result into the database
138
139
  await this._db.client().seeder.create({
139
140
  data: {
140
- checksum: seederHash,
141
+ checksum,
141
142
  seederName: this.seederName(seederFile),
142
143
  startedAt: start,
143
144
  finishedAt: new Date(),
@@ -161,8 +162,7 @@ class Seeder extends lifecycle_manager_1.LifecycleManager {
161
162
  }
162
163
  /** @internal */
163
164
  async seederHash(seederFile) {
164
- const seederContent = await promises_1.default.readFile(seederFile, 'utf8');
165
- return (0, common_1.makeHash)(seederContent);
165
+ return (0, common_1.makeHash)(node_fs_1.default.createReadStream(seederFile, 'utf8'));
166
166
  }
167
167
  /** @internal */
168
168
  seederName(seederFile) {
package/server/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './create-server';
2
2
  export * from './register-model';
3
3
  export * from './register-plugin';
4
4
  export * from './register-route';
5
+ export * from './virtual-projection';
package/server/index.js CHANGED
@@ -18,3 +18,4 @@ __exportStar(require("./create-server"), exports);
18
18
  __exportStar(require("./register-model"), exports);
19
19
  __exportStar(require("./register-plugin"), exports);
20
20
  __exportStar(require("./register-route"), exports);
21
+ __exportStar(require("./virtual-projection"), exports);
@@ -10,6 +10,7 @@ const common_1 = require("@appweaver/common");
10
10
  const context_1 = require("../context");
11
11
  const security_1 = require("../security");
12
12
  const errors_1 = require("../errors");
13
+ const virtual_projection_1 = require("./virtual-projection");
13
14
  function registerRoute(handler, config) {
14
15
  const routes = [];
15
16
  const tempServer = (0, fastify_1.default)({
@@ -37,6 +38,12 @@ function registerRoute(handler, config) {
37
38
  recaptcha: config?.recaptcha,
38
39
  recaptchaAction: config?.recaptchaAction
39
40
  });
41
+ // Automatically project virtual fields onto response payloads for routes
42
+ // whose response schemas reference resource models
43
+ const projectionPlan = (0, virtual_projection_1.buildVirtualProjectionPlan)(route.schema?.response, (id) => server.getSchema(id));
44
+ const projectionHook = Object.keys(projectionPlan).length > 0
45
+ ? (0, virtual_projection_1.createVirtualProjectionHook)(projectionPlan)
46
+ : undefined;
40
47
  // Merge received route schema with default values based on configuration
41
48
  const mergedRoute = {
42
49
  ...route,
@@ -61,6 +68,12 @@ function registerRoute(handler, config) {
61
68
  config?.recaptcha || config?.recaptchaAction ? recaptcha : undefined,
62
69
  ...((0, common_1.isArray)(route.onRequest) ? route.onRequest : [route.onRequest])
63
70
  ].filter((h) => h !== undefined),
71
+ preSerialization: [
72
+ ...((0, common_1.isArray)(route.preSerialization)
73
+ ? route.preSerialization
74
+ : [route.preSerialization]),
75
+ projectionHook
76
+ ].filter((h) => h !== undefined),
64
77
  config: {
65
78
  ...config,
66
79
  ...route.config
@@ -0,0 +1,26 @@
1
+ import { preSerializationAsyncHookHandler } from 'fastify';
2
+ export type VirtualProjectionEntry = {
3
+ path: string[];
4
+ resourceName: string;
5
+ };
6
+ export type VirtualProjectionPlan = Record<string, VirtualProjectionEntry[]>;
7
+ /**
8
+ * Builds a virtual field projection plan from route response schemas. Each 2xx response schema is walked, resolving
9
+ * `$ref` references through the provided schema lookup, and every location referencing a resource output model
10
+ * (`<Name>`, `<Name>Single` or `<Name>Multiple`) whose model defines virtual fields — directly or through nested
11
+ * relations and files — is recorded as a payload path to project before serialization.
12
+ *
13
+ * @param {Record<string, unknown>} [responseSchemas] - The route `schema.response` object keyed by status code.
14
+ * @param {(id: string) => unknown} getSchema - Lookup function resolving a schema `$id` to its schema object.
15
+ * @return {VirtualProjectionPlan} Projection entries grouped by response status code, empty when no resource output
16
+ * models with virtual fields are referenced.
17
+ */
18
+ export declare function buildVirtualProjectionPlan(responseSchemas: Record<string, unknown> | undefined, getSchema: (id: string) => unknown): VirtualProjectionPlan;
19
+ /**
20
+ * Creates a Fastify `preSerialization` hook that applies a virtual field projection plan to the response payload.
21
+ * Projection failures are logged and never fail the request — the original payload is returned instead.
22
+ *
23
+ * @param {VirtualProjectionPlan} plan - The projection plan built from the route response schemas.
24
+ * @return {preSerializationAsyncHookHandler} The `preSerialization` hook applying the plan.
25
+ */
26
+ export declare function createVirtualProjectionHook(plan: VirtualProjectionPlan): preSerializationAsyncHookHandler;
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildVirtualProjectionPlan = buildVirtualProjectionPlan;
4
+ exports.createVirtualProjectionHook = createVirtualProjectionHook;
5
+ const common_1 = require("@appweaver/common");
6
+ const context_1 = require("../context");
7
+ const utils_1 = require("../utils");
8
+ /** Path segment marking an array level inside a projection path. */
9
+ const ARRAY_SEGMENT = '[]';
10
+ /**
11
+ * Schema `$id` suffixes of resource output models that represent a single resource instance. Longer suffixes are
12
+ * matched first so that `PostSingle` resolves to the `Post` model instead of a (nonexistent) `PostSingle` model.
13
+ */
14
+ const INSTANCE_SUFFIXES = ['Single', 'Multiple', ''];
15
+ /**
16
+ * Builds a virtual field projection plan from route response schemas. Each 2xx response schema is walked, resolving
17
+ * `$ref` references through the provided schema lookup, and every location referencing a resource output model
18
+ * (`<Name>`, `<Name>Single` or `<Name>Multiple`) whose model defines virtual fields — directly or through nested
19
+ * relations and files — is recorded as a payload path to project before serialization.
20
+ *
21
+ * @param {Record<string, unknown>} [responseSchemas] - The route `schema.response` object keyed by status code.
22
+ * @param {(id: string) => unknown} getSchema - Lookup function resolving a schema `$id` to its schema object.
23
+ * @return {VirtualProjectionPlan} Projection entries grouped by response status code, empty when no resource output
24
+ * models with virtual fields are referenced.
25
+ */
26
+ function buildVirtualProjectionPlan(responseSchemas, getSchema) {
27
+ const plan = {};
28
+ for (const [status, schema] of Object.entries(responseSchemas ?? {})) {
29
+ if (!/^2(\d\d|xx)$/i.test(status)) {
30
+ continue;
31
+ }
32
+ const entries = [];
33
+ collectProjectionEntries(schema, [], entries, getSchema, new Set());
34
+ if (entries.length > 0) {
35
+ plan[status.toLowerCase()] = entries;
36
+ }
37
+ }
38
+ return plan;
39
+ }
40
+ /**
41
+ * Creates a Fastify `preSerialization` hook that applies a virtual field projection plan to the response payload.
42
+ * Projection failures are logged and never fail the request — the original payload is returned instead.
43
+ *
44
+ * @param {VirtualProjectionPlan} plan - The projection plan built from the route response schemas.
45
+ * @return {preSerializationAsyncHookHandler} The `preSerialization` hook applying the plan.
46
+ */
47
+ function createVirtualProjectionHook(plan) {
48
+ return async (_request, reply, payload) => {
49
+ const entries = plan[String(reply.statusCode)] ?? plan['2xx'];
50
+ if (!entries?.length || (!(0, common_1.isObject)(payload) && !(0, common_1.isArray)(payload))) {
51
+ return payload;
52
+ }
53
+ try {
54
+ let projected = payload;
55
+ for (const entry of entries) {
56
+ projected = applyProjection(projected, entry.path, 0, entry.resourceName);
57
+ }
58
+ return projected;
59
+ }
60
+ catch (e) {
61
+ common_1.logger.warn(e, 'Error projecting virtual fields on response payload');
62
+ return payload;
63
+ }
64
+ };
65
+ }
66
+ /**
67
+ * Recursively walks a response schema and collects the payload paths at which resource output models appear. Resolves
68
+ * `$ref` references through the schema lookup, unwraps OpenAPI media type `content` objects, descends into `allOf` /
69
+ * `anyOf` / `oneOf` branches, and appends an {@link ARRAY_SEGMENT} marker for every array level entered.
70
+ *
71
+ * @param {unknown} schema - The schema node currently being walked, ignored when it is not an object.
72
+ * @param {string[]} path - Payload path segments accumulated from the response root to the current node.
73
+ * @param {VirtualProjectionEntry[]} entries - Accumulator the collected projection entries are pushed into.
74
+ * @param {(id: string) => unknown} getSchema - Lookup function resolving a schema `$id` to its schema object.
75
+ * @param {Set<string>} visitedRefs - Schema `$id`s already resolved along the current descent, guarding against
76
+ * circular references.
77
+ * @return {void}
78
+ */
79
+ function collectProjectionEntries(schema, path, entries, getSchema, visitedRefs) {
80
+ if (!(0, common_1.isObject)(schema)) {
81
+ return;
82
+ }
83
+ if ((0, common_1.isString)(schema['$ref'])) {
84
+ const refName = schema['$ref'].replace(/#$/, '');
85
+ if (recordInstanceEntry(refName, path, entries)) {
86
+ return;
87
+ }
88
+ // Guard against circular schema references along the current descent
89
+ if (visitedRefs.has(refName)) {
90
+ return;
91
+ }
92
+ collectProjectionEntries(getSchema(refName), path, entries, getSchema, new Set([...visitedRefs, refName]));
93
+ return;
94
+ }
95
+ if ((0, common_1.isString)(schema['$id']) &&
96
+ recordInstanceEntry(schema['$id'], path, entries)) {
97
+ return;
98
+ }
99
+ // OpenAPI-style response objects wrap the schema in media type content
100
+ if ((0, common_1.isObject)(schema['content'])) {
101
+ for (const media of Object.values(schema['content'])) {
102
+ collectProjectionEntries(media?.schema, path, entries, getSchema, visitedRefs);
103
+ }
104
+ return;
105
+ }
106
+ for (const composite of ['allOf', 'anyOf', 'oneOf']) {
107
+ if ((0, common_1.isArray)(schema[composite])) {
108
+ for (const branch of schema[composite]) {
109
+ collectProjectionEntries(branch, path, entries, getSchema, visitedRefs);
110
+ }
111
+ }
112
+ }
113
+ if ((0, common_1.isObject)(schema['items'])) {
114
+ collectProjectionEntries(schema['items'], [...path, ARRAY_SEGMENT], entries, getSchema, visitedRefs);
115
+ }
116
+ if ((0, common_1.isObject)(schema['properties'])) {
117
+ for (const [key, property] of Object.entries(schema['properties'])) {
118
+ collectProjectionEntries(property, [...path, key], entries, getSchema, visitedRefs);
119
+ }
120
+ }
121
+ }
122
+ /**
123
+ * Records a projection entry when the schema name resolves to a resource output model. Returns whether the name
124
+ * denotes a resource instance schema, regardless of an entry being recorded, since resource model schemas are
125
+ * projected recursively and must not be walked any further.
126
+ *
127
+ * @param {string} name - The schema name (`$id` or `$ref`) to resolve to a resource output model.
128
+ * @param {string[]} path - Payload path at which the schema was encountered.
129
+ * @param {VirtualProjectionEntry[]} entries - Accumulator the entry is pushed into, unless an equal entry is already
130
+ * present or the resource has no projectable fields.
131
+ * @return {boolean} `true` when the name denotes a resource instance schema, `false` otherwise.
132
+ */
133
+ function recordInstanceEntry(name, path, entries) {
134
+ const resourceName = resolveInstanceResourceName(name);
135
+ if (!resourceName) {
136
+ return false;
137
+ }
138
+ const exists = entries.some((entry) => entry.resourceName === resourceName &&
139
+ entry.path.length === path.length &&
140
+ entry.path.every((segment, index) => segment === path[index]));
141
+ if (!exists && hasProjectableFields(resourceName, new Set())) {
142
+ entries.push({ path, resourceName });
143
+ }
144
+ return true;
145
+ }
146
+ /**
147
+ * Resolves a schema name to the name of the resource whose output model it represents, by stripping each of the
148
+ * {@link INSTANCE_SUFFIXES} in turn and returning the first candidate that matches a registered model.
149
+ *
150
+ * @param {string} name - The schema name (`$id` or `$ref`) to resolve.
151
+ * @return {string | undefined} The resource name, or `undefined` when the schema does not belong to a registered
152
+ * resource model.
153
+ */
154
+ function resolveInstanceResourceName(name) {
155
+ for (const suffix of INSTANCE_SUFFIXES) {
156
+ if (suffix && !name.endsWith(suffix)) {
157
+ continue;
158
+ }
159
+ const resourceName = suffix ? name.slice(0, -suffix.length) : name;
160
+ if (resourceName && (0, context_1.injectModel)(resourceName, false)) {
161
+ return resourceName;
162
+ }
163
+ }
164
+ return undefined;
165
+ }
166
+ /**
167
+ * Checks whether projecting a resource would set any values — the model defines virtual fields itself, or reaches a
168
+ * model with virtual fields through its relations or files.
169
+ *
170
+ * @param {string} resourceName - Name of the resource model to check.
171
+ * @param {Set<string>} visited - Resource names already checked along the current descent, guarding against circular
172
+ * relations.
173
+ * @return {boolean} `true` when the resource or any resource reachable through its relations or files defines virtual
174
+ * fields, `false` otherwise.
175
+ */
176
+ function hasProjectableFields(resourceName, visited) {
177
+ const resourceModel = (0, context_1.injectModel)(resourceName, false);
178
+ if (!resourceModel || visited.has(resourceName)) {
179
+ return false;
180
+ }
181
+ visited.add(resourceName);
182
+ if (Object.keys(resourceModel.config?.virtual ?? {}).length > 0) {
183
+ return true;
184
+ }
185
+ for (const schema of [
186
+ resourceModel.relationsModel,
187
+ resourceModel.filesModel
188
+ ]) {
189
+ const properties = (0, common_1.extractSchemaProperties)(schema) ?? {};
190
+ for (const key of Object.keys(properties)) {
191
+ const nestedName = (0, common_1.extractResourceName)((0, common_1.extractSchemaProperties)(schema, key));
192
+ if (nestedName && hasProjectableFields(nestedName, visited)) {
193
+ return true;
194
+ }
195
+ }
196
+ }
197
+ return false;
198
+ }
199
+ /**
200
+ * Walks a payload along a projection path and projects the resource virtual fields onto every object reached at its
201
+ * end, mapping over array elements for each {@link ARRAY_SEGMENT} marker. Objects along the path are mutated in place,
202
+ * while arrays are mapped into new ones.
203
+ *
204
+ * @param {any} value - The payload node currently being walked.
205
+ * @param {string[]} path - Payload path segments leading to the values to project.
206
+ * @param {number} index - Index of the path segment to apply at this level.
207
+ * @param {string} resourceName - Name of the resource model whose virtual fields are projected.
208
+ * @return {any} The payload node with virtual fields projected, or the node unchanged when the path does not resolve.
209
+ */
210
+ function applyProjection(value, path, index, resourceName) {
211
+ if (value === null || value === undefined) {
212
+ return value;
213
+ }
214
+ if (index === path.length) {
215
+ if ((0, common_1.isArray)(value)) {
216
+ return value.map((item) => (0, common_1.isObject)(item) ? (0, utils_1.projectVirtualFields)(item, resourceName) : item);
217
+ }
218
+ return (0, common_1.isObject)(value) ? (0, utils_1.projectVirtualFields)(value, resourceName) : value;
219
+ }
220
+ const segment = path[index];
221
+ if (segment === ARRAY_SEGMENT) {
222
+ return (0, common_1.isArray)(value)
223
+ ? value.map((item) => applyProjection(item, path, index + 1, resourceName))
224
+ : value;
225
+ }
226
+ if (!(0, common_1.isObject)(value) || value[segment] === undefined) {
227
+ return value;
228
+ }
229
+ value[segment] = applyProjection(value[segment], path, index + 1, resourceName);
230
+ return value;
231
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FileService = void 0;
4
+ const node_stream_1 = require("node:stream");
4
5
  const common_1 = require("@appweaver/common");
5
6
  const context_1 = require("../context");
6
7
  const errors_1 = require("../errors");
@@ -169,11 +170,28 @@ class FileService {
169
170
  if ((0, utils_1.isProcessableImage)(data.mimetype)) {
170
171
  fileStream = (0, utils_1.processImage)(data.file, data.mimetype, fileConfig.image);
171
172
  }
172
- const fileName = await this._storage.store(generatedName, fileStream);
173
+ // Tee the stream so the checksum is calculated over the exact bytes
174
+ // written to storage in a single pass, without buffering the file.
175
+ const storageStream = new node_stream_1.PassThrough();
176
+ const checksumStream = new node_stream_1.PassThrough();
177
+ fileStream.pipe(storageStream);
178
+ fileStream.pipe(checksumStream);
179
+ // pipe() does not forward source errors to destinations, so both branches
180
+ // must be destroyed manually to avoid hanging on a failed upload stream.
181
+ fileStream.on('error', (e) => {
182
+ common_1.logger.error(e, 'Error calculating file checksum');
183
+ storageStream.destroy(e);
184
+ checksumStream.destroy(e);
185
+ });
186
+ const [fileName, checksum] = await Promise.all([
187
+ this._storage.store(generatedName, storageStream),
188
+ (0, common_1.makeHash)(checksumStream)
189
+ ]);
173
190
  if (!fileName) {
174
191
  throw new errors_1.HttpError('Error saving file to storage', 500);
175
192
  }
176
193
  createFile.name = fileName;
194
+ createFile.checksum = checksum;
177
195
  // File size checks must come after storing a file due to bytesRead and
178
196
  // truncated fields being set only after reading the full file stream.
179
197
  const maxSizeBytes = (0, utils_1.sizeInBytes)(fileConfig.maxSize);
@@ -209,7 +227,7 @@ class FileService {
209
227
  if ((0, common_1.isArray)(result[data.fieldname])) {
210
228
  file = result[data.fieldname].find((f) => f.name === createFile.name);
211
229
  }
212
- file.url = this.buildFileUrl(file, policy);
230
+ file.url = (0, utils_1.buildFileUrl)(file);
213
231
  common_1.logger.debug({ file }, 'File saved');
214
232
  return file;
215
233
  }
@@ -288,7 +306,7 @@ class FileService {
288
306
  const deletedFile = (await this._db.client().file.delete({
289
307
  where: { name: fileName }
290
308
  }));
291
- deletedFile.url = this.buildFileUrl(deletedFile, policy);
309
+ deletedFile.url = (0, utils_1.buildFileUrl)(deletedFile);
292
310
  common_1.logger.debug({ deletedFile }, 'File deleted');
293
311
  return deletedFile;
294
312
  }
@@ -382,10 +400,6 @@ class FileService {
382
400
  return deletedFiles;
383
401
  }
384
402
  /** @internal */
385
- buildFileUrl(file, policy) {
386
- return `${common_1.config.APP_HOSTNAME}/files/${policy.accessType === 'public' ? 'public' : 'protected'}/${file.name}`;
387
- }
388
- /** @internal */
389
403
  async deleteSafe(fileName) {
390
404
  let success = true;
391
405
  try {
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const common_1 = require("@appweaver/common");
4
3
  const factory_1 = require("../../../factory");
5
- const context_1 = require("../../../context");
4
+ const utils_1 = require("../../../utils");
6
5
  exports.default = (0, factory_1.createModel)({
7
6
  name: 'File',
8
7
  scalars: {
@@ -26,6 +25,11 @@ exports.default = (0, factory_1.createModel)({
26
25
  minimum: 0,
27
26
  example: 1024
28
27
  },
28
+ checksum: {
29
+ type: 'string',
30
+ maxLength: 128,
31
+ example: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
32
+ },
29
33
  title: {
30
34
  type: 'string',
31
35
  maxLength: 511,
@@ -62,12 +66,7 @@ exports.default = (0, factory_1.createModel)({
62
66
  type: 'none'
63
67
  },
64
68
  output: {
65
- value: (file) => {
66
- const accessType = (0, context_1.injectPolicy)(file.resourceName ?? '', false)
67
- ?.files?.[file.resourceField ?? '']?.accessType;
68
- const pathPrefix = accessType === 'public' ? 'public' : 'protected';
69
- return `${common_1.config.APP_HOSTNAME}/files/${pathPrefix}/${file.name}`;
70
- }
69
+ value: (file) => (0, utils_1.buildFileUrl)(file)
71
70
  }
72
71
  }
73
72
  },
package/types/auth.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AuthScope, AuthSource } from '@appweaver/common';
1
+ import { AuthScope, AuthSource, AuthUser } from '@appweaver/common';
2
2
  export type JwtPayload = {
3
3
  scope: AuthScope;
4
4
  source: AuthSource;
@@ -24,8 +24,24 @@ export type TwoFactorAuthData = {
24
24
  export type OAuth2StateData = {
25
25
  redirectToUrl: string;
26
26
  };
27
+ export type UserInfo = {
28
+ id: string;
29
+ email: string;
30
+ firstName: string;
31
+ lastName: string;
32
+ avatarUrl?: string;
33
+ };
34
+ export type AvatarFile = {
35
+ name: string;
36
+ mimeType: string;
37
+ size: number;
38
+ data: Buffer;
39
+ };
27
40
  export type UserAdditionalData = {
28
41
  firstName: string;
29
42
  lastName: string;
43
+ avatarUrl?: string;
44
+ avatarFile?: AvatarFile;
30
45
  };
31
46
  export type RegistrationDataFn<T = any> = (source: AuthSource, email: string, password?: string, additionalData?: Partial<UserAdditionalData>) => T | Promise<T>;
47
+ export type CheckOAuth2UserFn = (source: AuthSource, userInfo: UserInfo, authUser: AuthUser | null) => void | string | Error | Promise<void | string | Error>;
@@ -139,6 +139,7 @@ export type File = {
139
139
  originalName: string;
140
140
  mimeType: string;
141
141
  sizeBytes: number;
142
+ checksum?: string | null;
142
143
  title?: string | null;
143
144
  description?: string | null;
144
145
  resourceField?: string | null;
@@ -155,6 +156,7 @@ export type FileSingle = {
155
156
  originalName: string;
156
157
  mimeType: string;
157
158
  sizeBytes: number;
159
+ checksum?: string | null;
158
160
  title?: string | null;
159
161
  description?: string | null;
160
162
  url: string;
@@ -168,6 +170,7 @@ export type FileMultiple = {
168
170
  originalName: string;
169
171
  mimeType: string;
170
172
  sizeBytes: number;
173
+ checksum?: string | null;
171
174
  title?: string | null;
172
175
  description?: string | null;
173
176
  url: string;
@@ -180,6 +183,7 @@ export type FileCreate = {
180
183
  originalName: string;
181
184
  mimeType: string;
182
185
  sizeBytes: number;
186
+ checksum?: string | null;
183
187
  title?: string | null;
184
188
  description?: string | null;
185
189
  };
@@ -188,6 +192,7 @@ export type FileUpdate = {
188
192
  originalName?: string;
189
193
  mimeType?: string;
190
194
  sizeBytes?: number;
195
+ checksum?: string | null;
191
196
  title?: string | null;
192
197
  description?: string | null;
193
198
  };
@@ -1,5 +1,14 @@
1
1
  import { FilesConfig } from '@appweaver/common';
2
2
  import { File } from '../types';
3
+ /**
4
+ * Builds the access URL for a stored file. The `public` or `protected` path prefix is resolved from the access type
5
+ * configured in the file policy of the resource owning the file, and the route prefix is taken from the
6
+ * `STORAGE_FILES_ROUTE_PREFIX` configuration.
7
+ *
8
+ * @param {File} file - The file object containing the name and owning resource information.
9
+ * @return {string} The absolute URL for accessing the file.
10
+ */
11
+ export declare function buildFileUrl(file: File): string;
3
12
  /**
4
13
  * Parses a range string and converts it into an object with start and end values.
5
14
  *
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFileUrl = buildFileUrl;
3
4
  exports.parseRange = parseRange;
4
5
  exports.maxFileSize = maxFileSize;
5
6
  exports.sizeInBytes = sizeInBytes;
@@ -8,6 +9,21 @@ exports.generateFileName = generateFileName;
8
9
  exports.sanitizeFilename = sanitizeFilename;
9
10
  exports.aggregateFiles = aggregateFiles;
10
11
  const common_1 = require("@appweaver/common");
12
+ const context_1 = require("../context");
13
+ /**
14
+ * Builds the access URL for a stored file. The `public` or `protected` path prefix is resolved from the access type
15
+ * configured in the file policy of the resource owning the file, and the route prefix is taken from the
16
+ * `STORAGE_FILES_ROUTE_PREFIX` configuration.
17
+ *
18
+ * @param {File} file - The file object containing the name and owning resource information.
19
+ * @return {string} The absolute URL for accessing the file.
20
+ */
21
+ function buildFileUrl(file) {
22
+ const accessType = (0, context_1.injectPolicy)(file.resourceName ?? '', false)?.files?.[file.resourceField ?? '']?.accessType;
23
+ const pathPrefix = accessType === 'public' ? 'public' : 'protected';
24
+ const routePrefix = `/${common_1.config.STORAGE_FILES_ROUTE_PREFIX}/`.replace(/\/+/g, '/');
25
+ return `${common_1.config.APP_HOSTNAME}${routePrefix}${pathPrefix}/${file.name}`;
26
+ }
11
27
  /**
12
28
  * Parses a range string and converts it into an object with start and end values.
13
29
  *
package/utils/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './export-util';
2
2
  export * from './file-util';
3
3
  export * from './image-util';
4
4
  export * from './schema-util';
5
+ export * from './virtual-util';
package/utils/index.js CHANGED
@@ -18,3 +18,4 @@ __exportStar(require("./export-util"), exports);
18
18
  __exportStar(require("./file-util"), exports);
19
19
  __exportStar(require("./image-util"), exports);
20
20
  __exportStar(require("./schema-util"), exports);
21
+ __exportStar(require("./virtual-util"), exports);
@@ -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;