@appweaver/core 1.0.25 → 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.
- package/package.json +1 -1
- package/prisma/client/internal/class.js +4 -4
- package/prisma/client/internal/prismaNamespace.d.ts +1 -0
- package/prisma/client/internal/prismaNamespace.js +1 -0
- package/prisma/client/internal/prismaNamespaceBrowser.d.ts +1 -0
- package/prisma/client/internal/prismaNamespaceBrowser.js +1 -0
- package/prisma/client/models/File.d.ts +29 -1
- package/resource/resource-service.d.ts +0 -1
- package/resource/resource-service.js +2 -39
- package/security/auth-service.d.ts +16 -1
- package/security/auth-service.js +28 -0
- package/security/create-auth-resources.d.ts +2 -1
- package/security/oauth2/create-oauth2-plugin.d.ts +2 -7
- package/security/oauth2/create-oauth2-plugin.js +45 -1
- package/security/oauth2/oauth2-custom.d.ts +1 -0
- package/security/oauth2/oauth2-custom.js +2 -1
- package/security/oauth2/oauth2-facebook.js +3 -2
- package/security/oauth2/oauth2-google.js +2 -1
- package/seeder/seeder.d.ts +1 -1
- package/seeder/seeder.js +5 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +1 -0
- package/server/register-route.js +13 -0
- package/server/virtual-projection.d.ts +26 -0
- package/server/virtual-projection.js +231 -0
- package/storage/file-service.js +21 -7
- package/storage/resources/file/model.js +7 -8
- package/types/auth.d.ts +17 -1
- package/types/generated.d.ts +5 -0
- package/utils/file-util.d.ts +9 -0
- package/utils/file-util.js +16 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/virtual-util.d.ts +11 -0
- 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
|
}
|
package/seeder/seeder.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
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);
|
package/server/register-route.js
CHANGED
|
@@ -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
|
+
}
|
package/storage/file-service.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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>;
|
package/types/generated.d.ts
CHANGED
|
@@ -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
|
};
|
package/utils/file-util.d.ts
CHANGED
|
@@ -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
|
*
|
package/utils/file-util.js
CHANGED
|
@@ -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
package/utils/index.js
CHANGED
|
@@ -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;
|