@directus/api 17.0.1 → 18.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/dist/app.js +8 -2
- package/dist/auth/drivers/ldap.js +14 -16
- package/dist/auth/drivers/local.js +16 -10
- package/dist/auth/drivers/oauth2.js +16 -11
- package/dist/auth/drivers/openid.js +16 -11
- package/dist/auth/drivers/saml.js +27 -12
- package/dist/cli/commands/init/index.js +3 -3
- package/dist/cli/commands/security/key.js +2 -2
- package/dist/cli/utils/create-env/env-stub.liquid +19 -4
- package/dist/cli/utils/create-env/index.js +2 -2
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +11 -4
- package/dist/controllers/auth.js +54 -19
- package/dist/controllers/extensions.js +102 -5
- package/dist/controllers/fields.js +0 -3
- package/dist/controllers/items.js +3 -2
- package/dist/controllers/permissions.js +1 -1
- package/dist/controllers/shares.js +19 -4
- package/dist/database/migrations/20220429A-add-flows.js +3 -3
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +2 -2
- package/dist/database/migrations/20240204A-marketplace.d.ts +3 -0
- package/dist/database/migrations/20240204A-marketplace.js +68 -0
- package/dist/database/migrations/run.js +3 -2
- package/dist/extensions/lib/get-extensions-settings.d.ts +6 -2
- package/dist/extensions/lib/get-extensions-settings.js +70 -22
- package/dist/extensions/lib/get-extensions.d.ts +5 -1
- package/dist/extensions/lib/get-extensions.js +7 -31
- package/dist/extensions/lib/installation/index.d.ts +2 -0
- package/dist/extensions/lib/installation/index.js +9 -0
- package/dist/extensions/lib/installation/manager.d.ts +5 -0
- package/dist/extensions/lib/installation/manager.js +90 -0
- package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
- package/dist/extensions/lib/sync-extensions.js +11 -10
- package/dist/extensions/manager.d.ts +27 -25
- package/dist/extensions/manager.js +214 -183
- package/dist/middleware/authenticate.d.ts +1 -0
- package/dist/middleware/error-handler.js +22 -18
- package/dist/middleware/extract-token.d.ts +6 -5
- package/dist/middleware/extract-token.js +27 -11
- package/dist/middleware/merge-content-versions.d.ts +2 -0
- package/dist/middleware/merge-content-versions.js +26 -0
- package/dist/middleware/respond.js +0 -12
- package/dist/middleware/validate-batch.d.ts +1 -0
- package/dist/operations/item-update/index.js +4 -1
- package/dist/request/agent-with-ip-validation.d.ts +1 -1
- package/dist/request/agent-with-ip-validation.js +5 -1
- package/dist/services/activity.js +3 -3
- package/dist/services/assets.js +2 -3
- package/dist/services/authentication.d.ts +7 -2
- package/dist/services/authentication.js +21 -13
- package/dist/services/extensions.d.ts +4 -8
- package/dist/services/extensions.js +110 -93
- package/dist/services/fields.js +34 -22
- package/dist/services/graphql/index.js +98 -42
- package/dist/services/import-export.js +61 -26
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/mail/index.d.ts +1 -1
- package/dist/services/mail/index.js +4 -2
- package/dist/services/payload.js +2 -2
- package/dist/services/{permissions.d.ts → permissions/index.d.ts} +3 -4
- package/dist/services/{permissions.js → permissions/index.js} +6 -23
- package/dist/services/permissions/lib/with-app-minimal-permissions.d.ts +2 -0
- package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
- package/dist/services/relations.d.ts +2 -3
- package/dist/services/relations.js +2 -2
- package/dist/services/roles.d.ts +9 -4
- package/dist/services/roles.js +51 -3
- package/dist/services/server.js +3 -0
- package/dist/services/shares.d.ts +3 -1
- package/dist/services/shares.js +9 -5
- package/dist/storage/index.js +5 -4
- package/dist/types/auth.d.ts +6 -4
- package/dist/types/graphql.d.ts +1 -0
- package/dist/utils/apply-query.js +3 -3
- package/dist/utils/filter-items.d.ts +2 -2
- package/dist/utils/filter-items.js +1 -3
- package/dist/utils/get-cache-headers.d.ts +1 -0
- package/dist/utils/get-cache-key.d.ts +1 -0
- package/dist/utils/get-graphql-query-and-variables.d.ts +1 -0
- package/dist/utils/get-ip-from-req.d.ts +1 -0
- package/dist/utils/get-milliseconds.d.ts +1 -1
- package/dist/utils/get-milliseconds.js +4 -1
- package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
- package/dist/utils/is-login-redirect-allowed.js +34 -0
- package/dist/utils/is-url-allowed.d.ts +1 -1
- package/dist/utils/is-url-allowed.js +5 -5
- package/dist/utils/is-valid-uuid.d.ts +3 -0
- package/dist/utils/is-valid-uuid.js +21 -0
- package/dist/utils/jwt.d.ts +1 -1
- package/dist/utils/jwt.js +3 -3
- package/dist/utils/merge-version-data.d.ts +3 -0
- package/dist/utils/merge-version-data.js +134 -0
- package/dist/utils/sanitize-query.js +2 -0
- package/dist/utils/should-skip-cache.d.ts +1 -0
- package/dist/utils/validate-keys.js +2 -2
- package/dist/utils/validate-query.js +1 -0
- package/dist/websocket/controllers/base.js +2 -2
- package/dist/websocket/controllers/hooks.js +1 -1
- package/package.json +50 -51
package/dist/services/fields.js
CHANGED
|
@@ -224,13 +224,15 @@ export class FieldsService {
|
|
|
224
224
|
accountability: this.accountability,
|
|
225
225
|
schema: this.schema,
|
|
226
226
|
});
|
|
227
|
-
const hookAdjustedField =
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
const hookAdjustedField = opts?.emitEvents !== false
|
|
228
|
+
? await emitter.emitFilter(`fields.create`, field, {
|
|
229
|
+
collection: collection,
|
|
230
|
+
}, {
|
|
231
|
+
database: trx,
|
|
232
|
+
schema: this.schema,
|
|
233
|
+
accountability: this.accountability,
|
|
234
|
+
})
|
|
235
|
+
: field;
|
|
234
236
|
if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
|
|
235
237
|
if (table) {
|
|
236
238
|
this.addColumnToTable(table, hookAdjustedField);
|
|
@@ -300,15 +302,23 @@ export class FieldsService {
|
|
|
300
302
|
}
|
|
301
303
|
const runPostColumnChange = await this.helpers.schema.preColumnChange();
|
|
302
304
|
const nestedActionEvents = [];
|
|
305
|
+
// 'type' is required for further checks on schema update
|
|
306
|
+
if (field.schema && !field.type) {
|
|
307
|
+
const existingType = this.schema.collections[collection]?.fields[field.field]?.type;
|
|
308
|
+
if (existingType)
|
|
309
|
+
field.type = existingType;
|
|
310
|
+
}
|
|
303
311
|
try {
|
|
304
|
-
const hookAdjustedField =
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
+
const hookAdjustedField = opts?.emitEvents !== false
|
|
313
|
+
? await emitter.emitFilter(`fields.update`, field, {
|
|
314
|
+
keys: [field.field],
|
|
315
|
+
collection: collection,
|
|
316
|
+
}, {
|
|
317
|
+
database: this.knex,
|
|
318
|
+
schema: this.schema,
|
|
319
|
+
accountability: this.accountability,
|
|
320
|
+
})
|
|
321
|
+
: field;
|
|
312
322
|
const record = field.meta
|
|
313
323
|
? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()
|
|
314
324
|
: null;
|
|
@@ -403,13 +413,15 @@ export class FieldsService {
|
|
|
403
413
|
const runPostColumnChange = await this.helpers.schema.preColumnChange();
|
|
404
414
|
const nestedActionEvents = [];
|
|
405
415
|
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
416
|
+
if (opts?.emitEvents !== false) {
|
|
417
|
+
await emitter.emitFilter('fields.delete', [field], {
|
|
418
|
+
collection: collection,
|
|
419
|
+
}, {
|
|
420
|
+
database: this.knex,
|
|
421
|
+
schema: this.schema,
|
|
422
|
+
accountability: this.accountability,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
413
425
|
await this.knex.transaction(async (trx) => {
|
|
414
426
|
const relations = this.schema.relations.filter((relation) => {
|
|
415
427
|
return ((relation.collection === collection && relation.field === field) ||
|
|
@@ -5,14 +5,14 @@ import { parseFilterFunctionPath, toBoolean } from '@directus/utils';
|
|
|
5
5
|
import argon2 from 'argon2';
|
|
6
6
|
import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
|
|
7
7
|
import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
|
|
8
|
-
import {
|
|
8
|
+
import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
|
|
9
9
|
import { clearSystemCache, getCache } from '../../cache.js';
|
|
10
|
-
import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
|
|
10
|
+
import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, } from '../../constants.js';
|
|
11
11
|
import getDatabase from '../../database/index.js';
|
|
12
12
|
import { generateHash } from '../../utils/generate-hash.js';
|
|
13
13
|
import { getGraphQLType } from '../../utils/get-graphql-type.js';
|
|
14
|
-
import { getMilliseconds } from '../../utils/get-milliseconds.js';
|
|
15
14
|
import { getService } from '../../utils/get-service.js';
|
|
15
|
+
import { mergeVersionsRaw, mergeVersionsRecursive } from '../../utils/merge-version-data.js';
|
|
16
16
|
import { reduceSchema } from '../../utils/reduce-schema.js';
|
|
17
17
|
import { sanitizeQuery } from '../../utils/sanitize-query.js';
|
|
18
18
|
import { validateQuery } from '../../utils/validate-query.js';
|
|
@@ -41,6 +41,8 @@ import { GraphQLVoid } from './types/void.js';
|
|
|
41
41
|
import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
|
|
42
42
|
import processError from './utils/process-error.js';
|
|
43
43
|
import { isSystemCollection } from '@directus/system-data';
|
|
44
|
+
import isDirectusJWT from '../../utils/is-directus-jwt.js';
|
|
45
|
+
import { verifyAccessJWT } from '../../utils/jwt.js';
|
|
44
46
|
const env = useEnv();
|
|
45
47
|
const validationRules = Array.from(specifiedRules);
|
|
46
48
|
if (env['GRAPHQL_INTROSPECTION'] === false) {
|
|
@@ -372,6 +374,14 @@ export class GraphQLService {
|
|
|
372
374
|
},
|
|
373
375
|
},
|
|
374
376
|
});
|
|
377
|
+
VersionTypes[relation.collection]?.addFields({
|
|
378
|
+
[relation.field]: {
|
|
379
|
+
type: GraphQLJSON,
|
|
380
|
+
resolve: (obj, _, __, info) => {
|
|
381
|
+
return obj[info?.path?.key ?? relation.field];
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
});
|
|
375
385
|
if (relation.meta?.one_field) {
|
|
376
386
|
CollectionTypes[relation.related_collection]?.addFields({
|
|
377
387
|
[relation.meta.one_field]: {
|
|
@@ -945,6 +955,7 @@ export class GraphQLService {
|
|
|
945
955
|
type: ReadCollectionTypes[collection.collection],
|
|
946
956
|
args: {
|
|
947
957
|
id: new GraphQLNonNull(GraphQLID),
|
|
958
|
+
version: GraphQLString,
|
|
948
959
|
},
|
|
949
960
|
resolve: async ({ info, context }) => {
|
|
950
961
|
const result = await self.resolveQuery(info);
|
|
@@ -1201,6 +1212,7 @@ export class GraphQLService {
|
|
|
1201
1212
|
return null;
|
|
1202
1213
|
const args = this.parseArgs(info.fieldNodes[0].arguments || [], info.variableValues);
|
|
1203
1214
|
let query;
|
|
1215
|
+
let versionRaw = false;
|
|
1204
1216
|
const isAggregate = collection.endsWith('_aggregated') && collection in this.schema.collections === false;
|
|
1205
1217
|
if (isAggregate) {
|
|
1206
1218
|
query = this.getAggregateQuery(args, selections);
|
|
@@ -1213,6 +1225,7 @@ export class GraphQLService {
|
|
|
1213
1225
|
}
|
|
1214
1226
|
if (collection.endsWith('_by_version') && collection in this.schema.collections === false) {
|
|
1215
1227
|
collection = collection.slice(0, -11);
|
|
1228
|
+
versionRaw = true;
|
|
1216
1229
|
}
|
|
1217
1230
|
}
|
|
1218
1231
|
if (args['id']) {
|
|
@@ -1240,12 +1253,16 @@ export class GraphQLService {
|
|
|
1240
1253
|
const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
|
|
1241
1254
|
if (saves) {
|
|
1242
1255
|
if (this.schema.collections[collection].singleton) {
|
|
1243
|
-
return
|
|
1256
|
+
return versionRaw
|
|
1257
|
+
? mergeVersionsRaw(result, saves)
|
|
1258
|
+
: mergeVersionsRecursive(result, saves, collection, this.schema);
|
|
1244
1259
|
}
|
|
1245
1260
|
else {
|
|
1246
1261
|
if (result?.[0] === undefined)
|
|
1247
1262
|
return null;
|
|
1248
|
-
return
|
|
1263
|
+
return versionRaw
|
|
1264
|
+
? mergeVersionsRaw(result[0], saves)
|
|
1265
|
+
: mergeVersionsRecursive(result[0], saves, collection, this.schema);
|
|
1249
1266
|
}
|
|
1250
1267
|
}
|
|
1251
1268
|
}
|
|
@@ -1587,6 +1604,7 @@ export class GraphQLService {
|
|
|
1587
1604
|
values: {
|
|
1588
1605
|
json: { value: 'json' },
|
|
1589
1606
|
cookie: { value: 'cookie' },
|
|
1607
|
+
session: { value: 'session' },
|
|
1590
1608
|
},
|
|
1591
1609
|
});
|
|
1592
1610
|
const ServerInfo = schemaComposer.createObjectTC({
|
|
@@ -1789,21 +1807,24 @@ export class GraphQLService {
|
|
|
1789
1807
|
accountability: accountability,
|
|
1790
1808
|
schema: this.schema,
|
|
1791
1809
|
});
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1810
|
+
const mode = args['mode'] ?? 'json';
|
|
1811
|
+
const { accessToken, refreshToken, expires } = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, {
|
|
1812
|
+
session: mode === 'session',
|
|
1813
|
+
otp: args?.otp,
|
|
1814
|
+
});
|
|
1815
|
+
const payload = { expires };
|
|
1816
|
+
if (mode === 'json') {
|
|
1817
|
+
payload.refresh_token = refreshToken;
|
|
1818
|
+
payload.access_token = accessToken;
|
|
1801
1819
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1820
|
+
if (mode === 'cookie') {
|
|
1821
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
1822
|
+
payload.access_token = accessToken;
|
|
1823
|
+
}
|
|
1824
|
+
if (mode === 'session') {
|
|
1825
|
+
res?.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
|
|
1826
|
+
}
|
|
1827
|
+
return payload;
|
|
1807
1828
|
},
|
|
1808
1829
|
},
|
|
1809
1830
|
auth_refresh: {
|
|
@@ -1826,35 +1847,51 @@ export class GraphQLService {
|
|
|
1826
1847
|
accountability: accountability,
|
|
1827
1848
|
schema: this.schema,
|
|
1828
1849
|
});
|
|
1829
|
-
const
|
|
1850
|
+
const mode = args['mode'] ?? 'json';
|
|
1851
|
+
let currentRefreshToken;
|
|
1852
|
+
if (mode === 'json') {
|
|
1853
|
+
currentRefreshToken = args['refresh_token'];
|
|
1854
|
+
}
|
|
1855
|
+
else if (mode === 'cookie') {
|
|
1856
|
+
currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
|
|
1857
|
+
}
|
|
1858
|
+
else if (mode === 'session') {
|
|
1859
|
+
const token = req?.cookies[env['SESSION_COOKIE_NAME']];
|
|
1860
|
+
if (isDirectusJWT(token)) {
|
|
1861
|
+
const payload = verifyAccessJWT(token, env['SECRET']);
|
|
1862
|
+
currentRefreshToken = payload.session;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1830
1865
|
if (!currentRefreshToken) {
|
|
1831
1866
|
throw new InvalidPayloadError({
|
|
1832
|
-
reason: `
|
|
1867
|
+
reason: `The refresh token is required in either the payload or cookie`,
|
|
1833
1868
|
});
|
|
1834
1869
|
}
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
|
|
1843
|
-
});
|
|
1870
|
+
const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, {
|
|
1871
|
+
session: mode === 'session',
|
|
1872
|
+
});
|
|
1873
|
+
const payload = { expires };
|
|
1874
|
+
if (mode === 'json') {
|
|
1875
|
+
payload.refresh_token = refreshToken;
|
|
1876
|
+
payload.access_token = accessToken;
|
|
1844
1877
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1878
|
+
if (mode === 'cookie') {
|
|
1879
|
+
res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
|
|
1880
|
+
payload.access_token = accessToken;
|
|
1881
|
+
}
|
|
1882
|
+
if (mode === 'session') {
|
|
1883
|
+
res?.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
|
|
1884
|
+
}
|
|
1885
|
+
return payload;
|
|
1850
1886
|
},
|
|
1851
1887
|
},
|
|
1852
1888
|
auth_logout: {
|
|
1853
1889
|
type: GraphQLBoolean,
|
|
1854
1890
|
args: {
|
|
1855
1891
|
refresh_token: GraphQLString,
|
|
1892
|
+
mode: AuthMode,
|
|
1856
1893
|
},
|
|
1857
|
-
resolve: async (_, args, { req }) => {
|
|
1894
|
+
resolve: async (_, args, { req, res }) => {
|
|
1858
1895
|
const accountability = { role: null };
|
|
1859
1896
|
if (req?.ip)
|
|
1860
1897
|
accountability.ip = req.ip;
|
|
@@ -1868,13 +1905,33 @@ export class GraphQLService {
|
|
|
1868
1905
|
accountability: accountability,
|
|
1869
1906
|
schema: this.schema,
|
|
1870
1907
|
});
|
|
1871
|
-
const
|
|
1908
|
+
const mode = args['mode'] ?? 'json';
|
|
1909
|
+
let currentRefreshToken;
|
|
1910
|
+
if (mode === 'json') {
|
|
1911
|
+
currentRefreshToken = args['refresh_token'];
|
|
1912
|
+
}
|
|
1913
|
+
else if (mode === 'cookie') {
|
|
1914
|
+
currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
|
|
1915
|
+
}
|
|
1916
|
+
else if (mode === 'session') {
|
|
1917
|
+
const token = req?.cookies[env['SESSION_COOKIE_NAME']];
|
|
1918
|
+
if (isDirectusJWT(token)) {
|
|
1919
|
+
const payload = verifyAccessJWT(token, env['SECRET']);
|
|
1920
|
+
currentRefreshToken = payload.session;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1872
1923
|
if (!currentRefreshToken) {
|
|
1873
1924
|
throw new InvalidPayloadError({
|
|
1874
|
-
reason: `
|
|
1925
|
+
reason: `The refresh token is required in either the payload or cookie`,
|
|
1875
1926
|
});
|
|
1876
1927
|
}
|
|
1877
1928
|
await authenticationService.logout(currentRefreshToken);
|
|
1929
|
+
if (req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
|
|
1930
|
+
res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
|
|
1931
|
+
}
|
|
1932
|
+
if (req?.cookies[env['SESSION_COOKIE_NAME']]) {
|
|
1933
|
+
res?.clearCookie(env['SESSION_COOKIE_NAME'], SESSION_COOKIE_OPTIONS);
|
|
1934
|
+
}
|
|
1878
1935
|
return true;
|
|
1879
1936
|
},
|
|
1880
1937
|
},
|
|
@@ -2479,8 +2536,7 @@ export class GraphQLService {
|
|
|
2479
2536
|
update_extensions_item: {
|
|
2480
2537
|
type: Extension,
|
|
2481
2538
|
args: {
|
|
2482
|
-
|
|
2483
|
-
name: new GraphQLNonNull(GraphQLString),
|
|
2539
|
+
id: GraphQLID,
|
|
2484
2540
|
data: toInputObjectType(schemaComposer.createObjectTC({
|
|
2485
2541
|
name: 'update_directus_extensions_input',
|
|
2486
2542
|
fields: {
|
|
@@ -2498,8 +2554,8 @@ export class GraphQLService {
|
|
|
2498
2554
|
accountability: this.accountability,
|
|
2499
2555
|
schema: this.schema,
|
|
2500
2556
|
});
|
|
2501
|
-
await extensionsService.updateOne(args['
|
|
2502
|
-
return await extensionsService.readOne(args['
|
|
2557
|
+
await extensionsService.updateOne(args['id'], args['data']);
|
|
2558
|
+
return await extensionsService.readOne(args['id']);
|
|
2503
2559
|
},
|
|
2504
2560
|
},
|
|
2505
2561
|
});
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
2
|
import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
|
|
3
|
+
import { isSystemCollection } from '@directus/system-data';
|
|
3
4
|
import { parseJSON, toArray } from '@directus/utils';
|
|
5
|
+
import { createTmpFile } from '@directus/utils/node';
|
|
4
6
|
import { queue } from 'async';
|
|
5
7
|
import destroyStream from 'destroy';
|
|
6
8
|
import { dump as toYAML } from 'js-yaml';
|
|
7
9
|
import { parse as toXML } from 'js2xmlparser';
|
|
8
10
|
import { Parser as CSVParser, transforms as CSVTransforms } from 'json2csv';
|
|
9
|
-
import { createReadStream } from 'node:fs';
|
|
11
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
10
12
|
import { appendFile } from 'node:fs/promises';
|
|
11
13
|
import Papa from 'papaparse';
|
|
12
14
|
import StreamArray from 'stream-json/streamers/StreamArray.js';
|
|
@@ -20,7 +22,6 @@ import { FilesService } from './files.js';
|
|
|
20
22
|
import { ItemsService } from './items.js';
|
|
21
23
|
import { NotificationsService } from './notifications.js';
|
|
22
24
|
import { UsersService } from './users.js';
|
|
23
|
-
import { isSystemCollection } from '@directus/system-data';
|
|
24
25
|
const env = useEnv();
|
|
25
26
|
const logger = useLogger();
|
|
26
27
|
export class ImportService {
|
|
@@ -86,7 +87,10 @@ export class ImportService {
|
|
|
86
87
|
});
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
|
-
importCSV(collection, stream) {
|
|
90
|
+
async importCSV(collection, stream) {
|
|
91
|
+
const tmpFile = await createTmpFile().catch(() => null);
|
|
92
|
+
if (!tmpFile)
|
|
93
|
+
throw new Error('Failed to create temporary file for import');
|
|
90
94
|
const nestedActionEvents = [];
|
|
91
95
|
return this.knex.transaction((trx) => {
|
|
92
96
|
const service = new ItemsService(collection, {
|
|
@@ -116,35 +120,66 @@ export class ImportService {
|
|
|
116
120
|
transform,
|
|
117
121
|
};
|
|
118
122
|
return new Promise((resolve, reject) => {
|
|
119
|
-
stream
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (obj[field] === undefined) {
|
|
125
|
-
delete obj[field];
|
|
123
|
+
const streams = [stream];
|
|
124
|
+
const cleanup = (destroy = true) => {
|
|
125
|
+
if (destroy) {
|
|
126
|
+
for (const stream of streams) {
|
|
127
|
+
destroyStream(stream);
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
tmpFile.cleanup().catch(() => {
|
|
131
|
+
logger.warn(`Failed to cleanup temporary import file (${tmpFile.path})`);
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
saveQueue.error((error) => {
|
|
135
|
+
reject(error);
|
|
136
|
+
});
|
|
137
|
+
const fileWriteStream = createWriteStream(tmpFile.path)
|
|
138
|
+
.on('error', (error) => {
|
|
139
|
+
cleanup();
|
|
140
|
+
reject(new Error('Error while writing import data to temporary file', { cause: error }));
|
|
133
141
|
})
|
|
134
|
-
.on('
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
.on('finish', () => {
|
|
143
|
+
const fileReadStream = createReadStream(tmpFile.path).on('error', (error) => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(new Error('Error while reading import data from temporary file', { cause: error }));
|
|
146
|
+
});
|
|
147
|
+
streams.push(fileReadStream);
|
|
148
|
+
fileReadStream
|
|
149
|
+
.pipe(Papa.parse(Papa.NODE_STREAM_INPUT, PapaOptions))
|
|
150
|
+
.on('data', (obj) => {
|
|
151
|
+
// Filter out all undefined fields
|
|
152
|
+
for (const field in obj) {
|
|
153
|
+
if (obj[field] === undefined) {
|
|
154
|
+
delete obj[field];
|
|
155
|
+
}
|
|
141
156
|
}
|
|
142
|
-
|
|
157
|
+
saveQueue.push(obj);
|
|
158
|
+
})
|
|
159
|
+
.on('error', (error) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(new InvalidPayloadError({ reason: error.message }));
|
|
162
|
+
})
|
|
163
|
+
.on('end', () => {
|
|
164
|
+
cleanup(false);
|
|
165
|
+
// In case of empty CSV file
|
|
166
|
+
if (!saveQueue.started)
|
|
167
|
+
return resolve();
|
|
168
|
+
saveQueue.drain(() => {
|
|
169
|
+
for (const nestedActionEvent of nestedActionEvents) {
|
|
170
|
+
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
171
|
+
}
|
|
172
|
+
return resolve();
|
|
173
|
+
});
|
|
143
174
|
});
|
|
144
175
|
});
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
streams.push(fileWriteStream);
|
|
177
|
+
stream
|
|
178
|
+
.on('error', (error) => {
|
|
179
|
+
cleanup();
|
|
180
|
+
reject(new Error('Error while retrieving import data', { cause: error }));
|
|
181
|
+
})
|
|
182
|
+
.pipe(fileWriteStream);
|
|
148
183
|
});
|
|
149
184
|
});
|
|
150
185
|
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export * from './notifications.js';
|
|
|
18
18
|
export * from './operations.js';
|
|
19
19
|
export * from './panels.js';
|
|
20
20
|
export * from './payload.js';
|
|
21
|
-
export * from './permissions.js';
|
|
21
|
+
export * from './permissions/index.js';
|
|
22
22
|
export * from './presets.js';
|
|
23
23
|
export * from './relations.js';
|
|
24
24
|
export * from './revisions.js';
|
package/dist/services/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export * from './notifications.js';
|
|
|
18
18
|
export * from './operations.js';
|
|
19
19
|
export * from './panels.js';
|
|
20
20
|
export * from './payload.js';
|
|
21
|
-
export * from './permissions.js';
|
|
21
|
+
export * from './permissions/index.js';
|
|
22
22
|
export * from './presets.js';
|
|
23
23
|
export * from './relations.js';
|
|
24
24
|
export * from './revisions.js';
|
|
@@ -14,7 +14,7 @@ export declare class MailService {
|
|
|
14
14
|
knex: Knex;
|
|
15
15
|
mailer: Transporter;
|
|
16
16
|
constructor(opts: AbstractServiceOptions);
|
|
17
|
-
send
|
|
17
|
+
send(options: EmailOptions): Promise<void>;
|
|
18
18
|
private renderTemplate;
|
|
19
19
|
private getDefaultTemplateData;
|
|
20
20
|
}
|
|
@@ -55,8 +55,10 @@ export class MailService {
|
|
|
55
55
|
.map((line) => line.trim())
|
|
56
56
|
.join('\n');
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
this.mailer.sendMail({ ...emailOptions, from, html }).catch((error) => {
|
|
59
|
+
logger.warn(`Email send failed:`);
|
|
60
|
+
logger.warn(error);
|
|
61
|
+
});
|
|
60
62
|
}
|
|
61
63
|
async renderTemplate(template, variables) {
|
|
62
64
|
const customTemplatePath = path.resolve(getExtensionsPath(), 'templates', template + '.liquid');
|
package/dist/services/payload.js
CHANGED
|
@@ -4,7 +4,7 @@ import { format, isValid, parseISO } from 'date-fns';
|
|
|
4
4
|
import { unflatten } from 'flat';
|
|
5
5
|
import Joi from 'joi';
|
|
6
6
|
import { clone, cloneDeep, isNil, isObject, isPlainObject, omit, pick } from 'lodash-es';
|
|
7
|
-
import {
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { parse as wktToGeoJSON } from 'wellknown';
|
|
9
9
|
import { getHelpers } from '../database/helpers/index.js';
|
|
10
10
|
import getDatabase from '../database/index.js';
|
|
@@ -39,7 +39,7 @@ export class PayloadService {
|
|
|
39
39
|
},
|
|
40
40
|
async uuid({ action, value }) {
|
|
41
41
|
if (action === 'create' && !value) {
|
|
42
|
-
return
|
|
42
|
+
return randomUUID();
|
|
43
43
|
}
|
|
44
44
|
return value;
|
|
45
45
|
},
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { ItemPermissions, PermissionsAction, Query } from '@directus/types';
|
|
2
2
|
import type Keyv from 'keyv';
|
|
3
|
-
import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '
|
|
4
|
-
import type { QueryOptions } from '
|
|
5
|
-
import { ItemsService } from '
|
|
3
|
+
import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../../types/index.js';
|
|
4
|
+
import type { QueryOptions } from '../items.js';
|
|
5
|
+
import { ItemsService } from '../items.js';
|
|
6
6
|
export declare class PermissionsService extends ItemsService {
|
|
7
7
|
systemCache: Keyv<any>;
|
|
8
8
|
constructor(options: AbstractServiceOptions);
|
|
9
9
|
getAllowedFields(action: PermissionsAction, collection?: string): Record<string, string[]>;
|
|
10
10
|
readByQuery(query: Query, opts?: QueryOptions): Promise<Partial<Item>[]>;
|
|
11
|
-
readMany(keys: PrimaryKey[], query?: Query, opts?: QueryOptions): Promise<Partial<Item>[]>;
|
|
12
11
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
13
12
|
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
14
13
|
updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { ForbiddenError } from '@directus/errors';
|
|
2
|
-
import { clearSystemCache, getCache } from '
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { ItemsService } from './items.js';
|
|
2
|
+
import { clearSystemCache, getCache } from '../../cache.js';
|
|
3
|
+
import { AuthorizationService } from '../authorization.js';
|
|
4
|
+
import { ItemsService } from '../items.js';
|
|
5
|
+
import { withAppMinimalPermissions } from './lib/with-app-minimal-permissions.js';
|
|
7
6
|
export class PermissionsService extends ItemsService {
|
|
8
7
|
systemCache;
|
|
9
8
|
constructor(options) {
|
|
@@ -30,24 +29,8 @@ export class PermissionsService extends ItemsService {
|
|
|
30
29
|
return fieldsPerCollection;
|
|
31
30
|
}
|
|
32
31
|
async readByQuery(query, opts) {
|
|
33
|
-
const result = await super.readByQuery(query, opts);
|
|
34
|
-
|
|
35
|
-
result.push(...filterItems(appAccessMinimalPermissions.map((permission) => ({
|
|
36
|
-
...permission,
|
|
37
|
-
role: this.accountability.role,
|
|
38
|
-
})), query.filter));
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
async readMany(keys, query = {}, opts) {
|
|
43
|
-
const result = await super.readMany(keys, query, opts);
|
|
44
|
-
if (this.accountability && this.accountability.app === true) {
|
|
45
|
-
result.push(...filterItems(appAccessMinimalPermissions.map((permission) => ({
|
|
46
|
-
...permission,
|
|
47
|
-
role: this.accountability.role,
|
|
48
|
-
})), query.filter));
|
|
49
|
-
}
|
|
50
|
-
return result;
|
|
32
|
+
const result = (await super.readByQuery(query, opts));
|
|
33
|
+
return withAppMinimalPermissions(this.accountability, result, query.filter);
|
|
51
34
|
}
|
|
52
35
|
async createOne(data, opts) {
|
|
53
36
|
const res = await super.createOne(data, opts);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { appAccessMinimalPermissions } from '@directus/system-data';
|
|
2
|
+
import { filterItems } from '../../../utils/filter-items.js';
|
|
3
|
+
import { mergePermissions } from '../../../utils/merge-permissions.js';
|
|
4
|
+
export function withAppMinimalPermissions(accountability, permissions, filter) {
|
|
5
|
+
if (accountability?.app === true) {
|
|
6
|
+
const filteredAppMinimalPermissions = filterItems(appAccessMinimalPermissions.map((permission) => ({
|
|
7
|
+
...permission,
|
|
8
|
+
role: accountability.role,
|
|
9
|
+
})), filter);
|
|
10
|
+
return mergePermissions('or', permissions, filteredAppMinimalPermissions);
|
|
11
|
+
}
|
|
12
|
+
return permissions;
|
|
13
|
+
}
|
|
@@ -4,9 +4,8 @@ import type Keyv from 'keyv';
|
|
|
4
4
|
import type { Knex } from 'knex';
|
|
5
5
|
import type { Helpers } from '../database/helpers/index.js';
|
|
6
6
|
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
7
|
-
import type
|
|
8
|
-
import {
|
|
9
|
-
import { PermissionsService } from './permissions.js';
|
|
7
|
+
import { ItemsService, type QueryOptions } from './items.js';
|
|
8
|
+
import { PermissionsService } from './permissions/index.js';
|
|
10
9
|
export declare class RelationsService {
|
|
11
10
|
knex: Knex;
|
|
12
11
|
permissionsService: PermissionsService;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
|
|
2
2
|
import { createInspector } from '@directus/schema';
|
|
3
|
+
import { systemRelationRows } from '@directus/system-data';
|
|
3
4
|
import { toArray } from '@directus/utils';
|
|
4
5
|
import { clearSystemCache, getCache } from '../cache.js';
|
|
5
6
|
import { getHelpers } from '../database/helpers/index.js';
|
|
@@ -8,8 +9,7 @@ import emitter from '../emitter.js';
|
|
|
8
9
|
import { getDefaultIndexName } from '../utils/get-default-index-name.js';
|
|
9
10
|
import { getSchema } from '../utils/get-schema.js';
|
|
10
11
|
import { ItemsService } from './items.js';
|
|
11
|
-
import { PermissionsService } from './permissions.js';
|
|
12
|
-
import { systemRelationRows } from '@directus/system-data';
|
|
12
|
+
import { PermissionsService } from './permissions/index.js';
|
|
13
13
|
export class RelationsService {
|
|
14
14
|
knex;
|
|
15
15
|
permissionsService;
|