@wbce-d9/api 12.6.0 → 12.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flows.d.ts +5 -0
- package/dist/flows.js +38 -7
- package/dist/services/fields.js +12 -0
- package/dist/services/payload.js +3 -0
- package/dist/services/specifications.js +2 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +10 -2
- package/dist/utils/package.d.ts +1 -1
- package/dist/utils/package.js +14 -2
- package/package.json +8 -8
package/dist/flows.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ declare class FlowManager {
|
|
|
15
15
|
runOperationFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<unknown>;
|
|
16
16
|
runWebhookFlow(id: string, data: unknown, context: Record<string, unknown>): Promise<unknown>;
|
|
17
17
|
private load;
|
|
18
|
+
/**
|
|
19
|
+
* Manual flows act on behalf of the triggering user: make sure that user can see the flow itself
|
|
20
|
+
* and read the collection / items passed in the payload before running it.
|
|
21
|
+
*/
|
|
22
|
+
private checkManualTriggerAccess;
|
|
18
23
|
private unload;
|
|
19
24
|
private executeFlow;
|
|
20
25
|
private executeOperation;
|
package/dist/flows.js
CHANGED
|
@@ -197,13 +197,16 @@ class FlowManager {
|
|
|
197
197
|
logger.warn(`Specified collection must be one of: ${enabledCollections.join(', ')}.`);
|
|
198
198
|
throw new exceptions.ForbiddenException();
|
|
199
199
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
200
|
+
const run = () => {
|
|
201
|
+
if (flow.options['async']) {
|
|
202
|
+
this.executeFlow(flow, data, context);
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
return this.executeFlow(flow, data, context);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
return this.checkManualTriggerAccess(flow, targetCollection, data, context).then(run);
|
|
207
210
|
};
|
|
208
211
|
// Default return to $last for manual
|
|
209
212
|
flow.options['return'] = '$last';
|
|
@@ -212,6 +215,34 @@ class FlowManager {
|
|
|
212
215
|
}
|
|
213
216
|
this.isLoaded = true;
|
|
214
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Manual flows act on behalf of the triggering user: make sure that user can see the flow itself
|
|
220
|
+
* and read the collection / items passed in the payload before running it.
|
|
221
|
+
*/
|
|
222
|
+
async checkManualTriggerAccess(flow, collection, data, context) {
|
|
223
|
+
const accountability = context['accountability'];
|
|
224
|
+
if (!accountability)
|
|
225
|
+
throw new exceptions.ForbiddenException();
|
|
226
|
+
if (accountability.admin)
|
|
227
|
+
return;
|
|
228
|
+
const database = context['database'] ?? getDatabase();
|
|
229
|
+
const schema = context['schema'] ?? (await getSchema({ database }));
|
|
230
|
+
const flowsService = new FlowsService({ accountability, knex: database, schema });
|
|
231
|
+
await flowsService.readOne(flow.id, { fields: ['id'] });
|
|
232
|
+
const keys = data?.['body']?.keys;
|
|
233
|
+
if (keys === null || keys === undefined || (Array.isArray(keys) && keys.length === 0)) {
|
|
234
|
+
if (flow.options?.['requireSelection'] !== false)
|
|
235
|
+
throw new exceptions.ForbiddenException();
|
|
236
|
+
const canReadCollection = accountability.permissions?.some((permission) => permission.collection === collection && permission.action === 'read');
|
|
237
|
+
if (!canReadCollection)
|
|
238
|
+
throw new exceptions.ForbiddenException();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!Array.isArray(keys))
|
|
242
|
+
throw new exceptions.ForbiddenException();
|
|
243
|
+
const authorizationService = new services.AuthorizationService({ accountability, knex: database, schema });
|
|
244
|
+
await authorizationService.checkAccess('read', collection, keys);
|
|
245
|
+
}
|
|
215
246
|
async unload() {
|
|
216
247
|
for (const trigger of this.triggerHandlers) {
|
|
217
248
|
trigger.events.forEach((event) => {
|
package/dist/services/fields.js
CHANGED
|
@@ -458,6 +458,18 @@ export class FieldsService {
|
|
|
458
458
|
if (Object.keys(collectionMetaUpdates).length > 0) {
|
|
459
459
|
await trx('directus_collections').update(collectionMetaUpdates).where({ collection });
|
|
460
460
|
}
|
|
461
|
+
const permissions = await trx('directus_permissions')
|
|
462
|
+
.select('id', 'fields')
|
|
463
|
+
.where({ collection });
|
|
464
|
+
for (const permission of permissions) {
|
|
465
|
+
const permittedFields = permission.fields?.split(',') ?? [];
|
|
466
|
+
if (permittedFields.includes(field) === false)
|
|
467
|
+
continue;
|
|
468
|
+
const remainingFields = permittedFields.filter((permittedField) => permittedField !== field);
|
|
469
|
+
await trx('directus_permissions')
|
|
470
|
+
.update({ fields: remainingFields.length > 0 ? remainingFields.join(',') : null })
|
|
471
|
+
.where({ id: permission.id });
|
|
472
|
+
}
|
|
461
473
|
// Cleanup directus_fields
|
|
462
474
|
const metaRow = await trx
|
|
463
475
|
.select('collection', 'field')
|
package/dist/services/payload.js
CHANGED
|
@@ -35,6 +35,9 @@ export class PayloadService {
|
|
|
35
35
|
if (action === 'create' || action === 'update') {
|
|
36
36
|
return await generateHash(String(value));
|
|
37
37
|
}
|
|
38
|
+
// Same as conceal
|
|
39
|
+
if (action === 'read')
|
|
40
|
+
return '**********';
|
|
38
41
|
return value;
|
|
39
42
|
},
|
|
40
43
|
async uuid({ action, value }) {
|
|
@@ -62,7 +62,8 @@ class OASSpecsService {
|
|
|
62
62
|
info: {
|
|
63
63
|
title: 'Dynamic API Specification',
|
|
64
64
|
description: 'This is a dynamically generated API specification for all endpoints existing on the current project.',
|
|
65
|
-
|
|
65
|
+
// Only admins see the real version, as on /server/info
|
|
66
|
+
version: this.accountability?.admin ? version : '0.0.0-hidden',
|
|
66
67
|
},
|
|
67
68
|
servers: [
|
|
68
69
|
{
|
|
@@ -32,4 +32,4 @@ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuer
|
|
|
32
32
|
};
|
|
33
33
|
export declare function addWhereClauses(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, filter: Filter, collection: string, aliasMap: AliasMap, logical?: 'and' | 'or'): void;
|
|
34
34
|
export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
|
|
35
|
-
export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
|
|
35
|
+
export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
|
|
@@ -33,7 +33,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
|
|
|
33
33
|
dbQuery.groupBy(query.group.map((column) => getColumn(knex, collection, column, false, schema)));
|
|
34
34
|
}
|
|
35
35
|
if (query.aggregate) {
|
|
36
|
-
applyAggregate(dbQuery, query.aggregate, collection);
|
|
36
|
+
applyAggregate(schema, dbQuery, query.aggregate, collection);
|
|
37
37
|
}
|
|
38
38
|
if (query.filter) {
|
|
39
39
|
hasMultiRelationalFilter = applyFilter(knex, schema, dbQuery, query.filter, collection, aliasMap).hasMultiRelationalFilter;
|
|
@@ -166,6 +166,9 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
|
|
|
166
166
|
rootQuery.orderBy(sortRecords);
|
|
167
167
|
return undefined;
|
|
168
168
|
}
|
|
169
|
+
function isConcealedField(schema, collection, field) {
|
|
170
|
+
return schema.collections[collection]?.fields[stripFunction(field)]?.special.includes('conceal') ?? false;
|
|
171
|
+
}
|
|
169
172
|
export function applyLimit(knex, rootQuery, limit) {
|
|
170
173
|
if (typeof limit === 'number') {
|
|
171
174
|
getHelpers(knex).schema.applyLimit(rootQuery, limit);
|
|
@@ -480,6 +483,8 @@ export async function applySearch(schema, dbQuery, searchQuery, collection) {
|
|
|
480
483
|
const fields = Object.entries(schema.collections[collection].fields);
|
|
481
484
|
dbQuery.andWhere(function () {
|
|
482
485
|
fields.forEach(([name, field]) => {
|
|
486
|
+
if (field.special.includes('conceal'))
|
|
487
|
+
return;
|
|
483
488
|
if (['text', 'string'].includes(field.type)) {
|
|
484
489
|
this.orWhereRaw(`LOWER(??) LIKE ?`, [`${collection}.${name}`, `%${searchQuery.toLowerCase()}%`]);
|
|
485
490
|
}
|
|
@@ -494,11 +499,14 @@ export async function applySearch(schema, dbQuery, searchQuery, collection) {
|
|
|
494
499
|
});
|
|
495
500
|
});
|
|
496
501
|
}
|
|
497
|
-
export function applyAggregate(dbQuery, aggregate, collection) {
|
|
502
|
+
export function applyAggregate(schema, dbQuery, aggregate, collection) {
|
|
498
503
|
for (const [operation, fields] of Object.entries(aggregate)) {
|
|
499
504
|
if (!fields)
|
|
500
505
|
continue;
|
|
501
506
|
for (const field of fields) {
|
|
507
|
+
if (!operation.startsWith('count') && isConcealedField(schema, collection, field)) {
|
|
508
|
+
throw new InvalidQueryException(`Field with "conceal" special does not allow the "${operation}" aggregate function`);
|
|
509
|
+
}
|
|
502
510
|
if (operation === 'avg') {
|
|
503
511
|
dbQuery.avg(`${collection}.${field}`, { as: `avg->${field}` });
|
|
504
512
|
}
|
package/dist/utils/package.d.ts
CHANGED
package/dist/utils/package.js
CHANGED
|
@@ -4,6 +4,18 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
5
|
const apiPkgPath = resolve(__dirname, '../../package.json');
|
|
6
6
|
const { name, version } = JSON.parse(readFileSync(apiPkgPath, 'utf8'));
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function pkgVersion(path) {
|
|
8
|
+
try {
|
|
9
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')).version ?? null : null;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// d9 release version (the bundle, matching the Docker image tag): dev-checkout root, then the
|
|
16
|
+
// installed bundle (published/projen image), then the api version as a last resort.
|
|
17
|
+
const mainVersion = pkgVersion(resolve(dirname(apiPkgPath), '../../../../../../package.json')) ??
|
|
18
|
+
pkgVersion(resolve(process.cwd(), 'node_modules/@wbce-d9/directus9/package.json')) ??
|
|
19
|
+
pkgVersion(resolve(process.cwd(), 'node_modules/directus9/package.json')) ??
|
|
20
|
+
version;
|
|
9
21
|
export { name, version, mainVersion };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wbce-d9/api",
|
|
3
|
-
"version": "12.6.
|
|
3
|
+
"version": "12.6.1",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -144,20 +144,20 @@
|
|
|
144
144
|
"tmp-promise": "3.0.3",
|
|
145
145
|
"tsx": "3.12.6",
|
|
146
146
|
"uuid-validate": "0.0.3",
|
|
147
|
-
"@wbce-d9/app": "11.0
|
|
148
|
-
"@wbce-d9/exceptions": "10.0.1",
|
|
149
|
-
"@wbce-d9/extensions-sdk": "10.0.5",
|
|
150
|
-
"@wbce-d9/specs": "9.26.5",
|
|
147
|
+
"@wbce-d9/app": "11.1.0",
|
|
151
148
|
"@wbce-d9/constants": "10.0.0",
|
|
149
|
+
"@wbce-d9/exceptions": "10.0.1",
|
|
152
150
|
"@wbce-d9/schema": "10.0.0",
|
|
153
|
-
"@wbce-d9/storage-driver-azure": "10.0.4",
|
|
154
151
|
"@wbce-d9/storage": "10.0.0",
|
|
152
|
+
"@wbce-d9/specs": "9.26.5",
|
|
153
|
+
"@wbce-d9/storage-driver-azure": "10.0.4",
|
|
155
154
|
"@wbce-d9/storage-driver-cloudinary": "10.0.5",
|
|
155
|
+
"@wbce-d9/extensions-sdk": "10.0.5",
|
|
156
|
+
"@wbce-d9/storage-driver-local": "10.0.4",
|
|
156
157
|
"@wbce-d9/storage-driver-gcs": "10.0.4",
|
|
157
158
|
"@wbce-d9/storage-driver-s3": "10.0.4",
|
|
158
159
|
"@wbce-d9/update-check": "10.0.4",
|
|
159
|
-
"@wbce-d9/utils": "10.0.4"
|
|
160
|
-
"@wbce-d9/storage-driver-local": "10.0.4"
|
|
160
|
+
"@wbce-d9/utils": "10.0.4"
|
|
161
161
|
},
|
|
162
162
|
"devDependencies": {
|
|
163
163
|
"@ngneat/falso": "6.4.0",
|