@directus/api 23.2.1 → 23.3.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/auth/drivers/oauth2.js +2 -2
- package/dist/auth/drivers/openid.d.ts +2 -0
- package/dist/auth/drivers/openid.js +32 -3
- package/dist/cli/commands/roles/create.d.ts +2 -1
- package/dist/cli/commands/roles/create.js +18 -4
- package/dist/cli/index.js +1 -0
- package/dist/database/helpers/date/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/date/dialects/oracle.js +14 -0
- package/dist/database/index.js +10 -0
- package/dist/database/migrations/20240924A-migrate-legacy-comments.js +23 -1
- package/dist/database/run-ast/utils/apply-parent-filters.js +0 -4
- package/dist/database/run-ast/utils/merge-with-parent-items.js +45 -18
- package/dist/permissions/utils/fetch-dynamic-variable-context.js +2 -2
- package/dist/schedules/retention.js +16 -2
- package/dist/services/collections.js +9 -9
- package/dist/services/files.js +2 -2
- package/dist/services/payload.js +4 -4
- package/dist/services/roles.js +2 -2
- package/dist/services/tus/data-store.js +14 -14
- package/dist/types/rolemap.d.ts +3 -0
- package/dist/types/rolemap.js +1 -0
- package/dist/utils/apply-snapshot.js +3 -4
- package/dist/websocket/handlers/index.js +1 -1
- package/package.json +11 -11
|
@@ -141,7 +141,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
141
141
|
const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
|
|
142
142
|
identifier,
|
|
143
143
|
provider: this.config['provider'],
|
|
144
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
144
|
+
providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
|
|
145
145
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
146
146
|
// Update user to update refresh_token and other properties that might have changed
|
|
147
147
|
if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
|
|
@@ -159,7 +159,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
|
|
|
159
159
|
const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
|
|
160
160
|
identifier,
|
|
161
161
|
provider: this.config['provider'],
|
|
162
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
162
|
+
providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
|
|
163
163
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
164
164
|
try {
|
|
165
165
|
await this.usersService.createOne(updatedUserPayload);
|
|
@@ -2,12 +2,14 @@ import { Router } from 'express';
|
|
|
2
2
|
import type { Client } from 'openid-client';
|
|
3
3
|
import { UsersService } from '../../services/users.js';
|
|
4
4
|
import type { AuthDriverOptions, User } from '../../types/index.js';
|
|
5
|
+
import type { RoleMap } from '../../types/rolemap.js';
|
|
5
6
|
import { LocalAuthDriver } from './local.js';
|
|
6
7
|
export declare class OpenIDAuthDriver extends LocalAuthDriver {
|
|
7
8
|
client: Promise<Client>;
|
|
8
9
|
redirectUrl: string;
|
|
9
10
|
usersService: UsersService;
|
|
10
11
|
config: Record<string, any>;
|
|
12
|
+
roleMap: RoleMap;
|
|
11
13
|
constructor(options: AuthDriverOptions, config: Record<string, any>);
|
|
12
14
|
generateCodeVerifier(): string;
|
|
13
15
|
generateAuthUrl(codeVerifier: string, prompt?: boolean): Promise<string>;
|
|
@@ -26,6 +26,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
26
26
|
redirectUrl;
|
|
27
27
|
usersService;
|
|
28
28
|
config;
|
|
29
|
+
roleMap;
|
|
29
30
|
constructor(options, config) {
|
|
30
31
|
super(options, config);
|
|
31
32
|
const env = useEnv();
|
|
@@ -40,6 +41,17 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
40
41
|
this.redirectUrl = redirectUrl.toString();
|
|
41
42
|
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
|
|
42
43
|
this.config = additionalConfig;
|
|
44
|
+
this.roleMap = {};
|
|
45
|
+
const roleMapping = this.config['roleMapping'];
|
|
46
|
+
if (roleMapping) {
|
|
47
|
+
this.roleMap = roleMapping;
|
|
48
|
+
}
|
|
49
|
+
// role mapping will fail on login if AUTH_<provider>_ROLE_MAPPING is an array instead of an object.
|
|
50
|
+
// This happens if the 'json:' prefix is missing from the variable declaration. To save the user from exhaustive debugging, we'll try to fail early here.
|
|
51
|
+
if (roleMapping instanceof Array) {
|
|
52
|
+
logger.error("[OpenID] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.");
|
|
53
|
+
throw new InvalidProviderError();
|
|
54
|
+
}
|
|
43
55
|
this.client = new Promise((resolve, reject) => {
|
|
44
56
|
Issuer.discover(issuerUrl)
|
|
45
57
|
.then((issuer) => {
|
|
@@ -123,6 +135,21 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
123
135
|
catch (e) {
|
|
124
136
|
throw handleError(e);
|
|
125
137
|
}
|
|
138
|
+
let role = this.config['defaultRoleId'];
|
|
139
|
+
const groupClaimName = this.config['groupClaimName'] ?? 'groups';
|
|
140
|
+
const groups = userInfo[groupClaimName];
|
|
141
|
+
if (Array.isArray(groups)) {
|
|
142
|
+
for (const key in this.roleMap) {
|
|
143
|
+
if (groups.includes(key)) {
|
|
144
|
+
// Overwrite default role if user is member of a group specified in roleMap
|
|
145
|
+
role = this.roleMap[key];
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
logger.debug(`[OpenID] Configured group claim with name "${groupClaimName}" does not exist or is empty.`);
|
|
152
|
+
}
|
|
126
153
|
// Flatten response to support dot indexes
|
|
127
154
|
userInfo = flatten(userInfo);
|
|
128
155
|
const { provider, identifierKey, allowPublicRegistration, requireVerifiedEmail, syncUserInfo } = this.config;
|
|
@@ -139,7 +166,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
139
166
|
last_name: userInfo['family_name'],
|
|
140
167
|
email: email,
|
|
141
168
|
external_identifier: identifier,
|
|
142
|
-
role:
|
|
169
|
+
role: role,
|
|
143
170
|
auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
|
|
144
171
|
};
|
|
145
172
|
const userId = await this.fetchUserId(identifier);
|
|
@@ -148,6 +175,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
148
175
|
// user that is about to be updated
|
|
149
176
|
let emitPayload = {
|
|
150
177
|
auth_data: userPayload.auth_data,
|
|
178
|
+
// Make sure a user's role gets updated if his openid group or role mapping changes
|
|
179
|
+
role: role,
|
|
151
180
|
};
|
|
152
181
|
if (syncUserInfo) {
|
|
153
182
|
emitPayload = {
|
|
@@ -160,7 +189,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
160
189
|
const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
|
|
161
190
|
identifier,
|
|
162
191
|
provider: this.config['provider'],
|
|
163
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
192
|
+
providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
|
|
164
193
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
165
194
|
// Update user to update refresh_token and other properties that might have changed
|
|
166
195
|
if (Object.values(updatedUserPayload).some((value) => value !== undefined)) {
|
|
@@ -179,7 +208,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
|
|
|
179
208
|
const updatedUserPayload = await emitter.emitFilter(`auth.create`, userPayload, {
|
|
180
209
|
identifier,
|
|
181
210
|
provider: this.config['provider'],
|
|
182
|
-
providerPayload: { accessToken: tokenSet.access_token, userInfo },
|
|
211
|
+
providerPayload: { accessToken: tokenSet.access_token, idToken: tokenSet.id_token, userInfo },
|
|
183
212
|
}, { database: getDatabase(), schema: this.schema, accountability: null });
|
|
184
213
|
try {
|
|
185
214
|
await this.usersService.createOne(updatedUserPayload);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { getSchema } from '../../../utils/get-schema.js';
|
|
2
2
|
import { RolesService } from '../../../services/roles.js';
|
|
3
|
+
import { PoliciesService } from '../../../services/index.js';
|
|
4
|
+
import { AccessService } from '../../../services/index.js';
|
|
3
5
|
import getDatabase from '../../../database/index.js';
|
|
4
6
|
import { useLogger } from '../../../logger/index.js';
|
|
5
|
-
export default async function rolesCreate({ role: name, admin }) {
|
|
7
|
+
export default async function rolesCreate({ role: name, admin, app, }) {
|
|
6
8
|
const database = getDatabase();
|
|
7
9
|
const logger = useLogger();
|
|
8
10
|
if (!name) {
|
|
@@ -11,9 +13,21 @@ export default async function rolesCreate({ role: name, admin }) {
|
|
|
11
13
|
}
|
|
12
14
|
try {
|
|
13
15
|
const schema = await getSchema();
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
16
|
+
const rolesService = new RolesService({ schema: schema, knex: database });
|
|
17
|
+
const policiesService = new PoliciesService({ schema: schema, knex: database });
|
|
18
|
+
const accessService = new AccessService({ schema: schema, knex: database });
|
|
19
|
+
const adminPolicyId = await policiesService.createOne({
|
|
20
|
+
name: `Policy for ${name}`,
|
|
21
|
+
admin_access: admin,
|
|
22
|
+
app_access: app,
|
|
23
|
+
icon: 'supervised_user_circle',
|
|
24
|
+
});
|
|
25
|
+
const roleId = await rolesService.createOne({ name });
|
|
26
|
+
await accessService.createOne({
|
|
27
|
+
role: roleId,
|
|
28
|
+
policy: adminPolicyId,
|
|
29
|
+
});
|
|
30
|
+
process.stdout.write(`${String(roleId)}\n`);
|
|
17
31
|
database.destroy();
|
|
18
32
|
process.exit(0);
|
|
19
33
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export async function createCli() {
|
|
|
61
61
|
.description('Create a new role')
|
|
62
62
|
.option('--role <value>', `name for the role`)
|
|
63
63
|
.option('--admin', `whether or not the role has admin access`)
|
|
64
|
+
.option('--app', `whether or not the role has app access`)
|
|
64
65
|
.action(rolesCreate);
|
|
65
66
|
program.command('count <collection>').description('Count the amount of items in a given collection').action(count);
|
|
66
67
|
program
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { DateHelper } from '../types.js';
|
|
2
2
|
export class DateHelperOracle extends DateHelper {
|
|
3
|
+
// Required to handle timezoned offset
|
|
4
|
+
parse(date) {
|
|
5
|
+
if (!date) {
|
|
6
|
+
return date;
|
|
7
|
+
}
|
|
8
|
+
if (date instanceof Date) {
|
|
9
|
+
return String(date.toISOString());
|
|
10
|
+
}
|
|
11
|
+
// Return YY-MM-DD as is for date support
|
|
12
|
+
if (date.length <= 10 && date.includes('-')) {
|
|
13
|
+
return date;
|
|
14
|
+
}
|
|
15
|
+
return String(new Date(date).toISOString());
|
|
16
|
+
}
|
|
3
17
|
fieldFlagForField(fieldType) {
|
|
4
18
|
switch (fieldType) {
|
|
5
19
|
case 'dateTime':
|
package/dist/database/index.js
CHANGED
|
@@ -106,6 +106,16 @@ export function getDatabase() {
|
|
|
106
106
|
callback(null, conn);
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
|
+
if (client === 'oracledb') {
|
|
110
|
+
poolConfig.afterCreate = async (conn, callback) => {
|
|
111
|
+
logger.trace('Setting OracleDB NLS_DATE_FORMAT and NLS_TIMESTAMP_FORMAT');
|
|
112
|
+
// enforce proper ISO standard 2024-12-10T10:54:00.123Z for datetime/timestamp
|
|
113
|
+
await conn.executeAsync('ALTER SESSION SET NLS_TIMESTAMP_FORMAT = \'YYYY-MM-DD"T"HH24:MI:SS.FF3"Z"\'');
|
|
114
|
+
// enforce 2024-12-10 date formet
|
|
115
|
+
await conn.executeAsync("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
|
|
116
|
+
callback(null, conn);
|
|
117
|
+
};
|
|
118
|
+
}
|
|
109
119
|
if (client === 'mysql') {
|
|
110
120
|
// Remove the conflicting `filename` option, defined by default in the Docker Image
|
|
111
121
|
if (isObject(knexConfig.connection))
|
|
@@ -12,6 +12,8 @@ export async function up(knex) {
|
|
|
12
12
|
}
|
|
13
13
|
const rowsLimit = 50;
|
|
14
14
|
let hasMore = true;
|
|
15
|
+
const existingUsers = new Set();
|
|
16
|
+
const missingUsers = new Set();
|
|
15
17
|
while (hasMore) {
|
|
16
18
|
const legacyComments = await knex
|
|
17
19
|
.select('*')
|
|
@@ -28,12 +30,32 @@ export async function up(knex) {
|
|
|
28
30
|
// Migrate legacy comment
|
|
29
31
|
if (legacyComment['action'] === Action.COMMENT) {
|
|
30
32
|
primaryKey = randomUUID();
|
|
33
|
+
let legacyCommentUserId = legacyComment.user;
|
|
34
|
+
if (legacyCommentUserId) {
|
|
35
|
+
if (missingUsers.has(legacyCommentUserId)) {
|
|
36
|
+
legacyCommentUserId = null;
|
|
37
|
+
}
|
|
38
|
+
else if (!existingUsers.has(legacyCommentUserId)) {
|
|
39
|
+
const userExists = await trx
|
|
40
|
+
.select('id')
|
|
41
|
+
.from('directus_users')
|
|
42
|
+
.where('id', '=', legacyCommentUserId)
|
|
43
|
+
.first();
|
|
44
|
+
if (userExists) {
|
|
45
|
+
existingUsers.add(legacyCommentUserId);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
missingUsers.add(legacyCommentUserId);
|
|
49
|
+
legacyCommentUserId = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
31
53
|
await trx('directus_comments').insert({
|
|
32
54
|
id: primaryKey,
|
|
33
55
|
collection: legacyComment.collection,
|
|
34
56
|
item: legacyComment.item,
|
|
35
57
|
comment: legacyComment.comment,
|
|
36
|
-
user_created:
|
|
58
|
+
user_created: legacyCommentUserId,
|
|
37
59
|
date_created: legacyComment.timestamp,
|
|
38
60
|
});
|
|
39
61
|
await trx('directus_activity')
|
|
@@ -33,10 +33,6 @@ export function applyParentFilters(schema, nestedCollectionNodes, parentItem) {
|
|
|
33
33
|
const foreignField = nestedNode.relation.field;
|
|
34
34
|
const foreignIds = uniq(parentItems.map((res) => res[nestedNode.parentKey])).filter((id) => !isNil(id));
|
|
35
35
|
merge(nestedNode, { query: { filter: { [foreignField]: { _in: foreignIds } } } });
|
|
36
|
-
if (nestedNode.relation.meta?.junction_field) {
|
|
37
|
-
const junctionField = nestedNode.relation.meta.junction_field;
|
|
38
|
-
merge(nestedNode, { query: { filter: { [junctionField]: { _nnull: true } } } });
|
|
39
|
-
}
|
|
40
36
|
}
|
|
41
37
|
else if (nestedNode.type === 'a2o') {
|
|
42
38
|
const keysPerCollection = {};
|
|
@@ -6,32 +6,59 @@ export function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode,
|
|
|
6
6
|
const nestedItems = toArray(nestedItem);
|
|
7
7
|
const parentItems = clone(toArray(parentItem));
|
|
8
8
|
if (nestedNode.type === 'm2o') {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const parentsByForeignKey = new Map();
|
|
10
|
+
parentItems.forEach((parentItem) => {
|
|
11
|
+
const relationKey = parentItem[nestedNode.relation.field];
|
|
12
|
+
if (!parentsByForeignKey.has(relationKey)) {
|
|
13
|
+
parentsByForeignKey.set(relationKey, []);
|
|
14
|
+
}
|
|
15
|
+
parentItem[nestedNode.fieldKey] = null;
|
|
16
|
+
parentsByForeignKey.get(relationKey).push(parentItem);
|
|
17
|
+
});
|
|
18
|
+
const nestPrimaryKeyField = schema.collections[nestedNode.relation.related_collection].primary;
|
|
19
|
+
for (const nestedItem of nestedItems) {
|
|
20
|
+
const nestedPK = nestedItem[nestPrimaryKeyField];
|
|
21
|
+
for (const parentItem of parentsByForeignKey.get(nestedPK)) {
|
|
22
|
+
parentItem[nestedNode.fieldKey] = nestedItem;
|
|
23
|
+
}
|
|
15
24
|
}
|
|
16
25
|
}
|
|
17
26
|
else if (nestedNode.type === 'o2m') {
|
|
27
|
+
const parentCollectionName = nestedNode.relation.related_collection;
|
|
28
|
+
const parentPrimaryKeyField = schema.collections[parentCollectionName].primary;
|
|
29
|
+
const parentRelationField = nestedNode.fieldKey;
|
|
30
|
+
const nestedParentKeyField = nestedNode.relation.field;
|
|
31
|
+
const parentsByPrimaryKey = new Map();
|
|
32
|
+
parentItems.forEach((parentItem) => {
|
|
33
|
+
if (!parentItem[parentRelationField])
|
|
34
|
+
parentItem[parentRelationField] = [];
|
|
35
|
+
const parentPrimaryKey = parentItem[parentPrimaryKeyField];
|
|
36
|
+
if (parentsByPrimaryKey.has(parentPrimaryKey)) {
|
|
37
|
+
throw new Error(`Duplicate parent primary key '${parentPrimaryKey}' of '${parentCollectionName}' when merging o2m nested items`);
|
|
38
|
+
}
|
|
39
|
+
parentsByPrimaryKey.set(parentPrimaryKey, parentItem);
|
|
40
|
+
});
|
|
41
|
+
const toAddToAllParents = [];
|
|
42
|
+
nestedItems.forEach((nestedItem) => {
|
|
43
|
+
if (nestedItem === null)
|
|
44
|
+
return;
|
|
45
|
+
if (Array.isArray(nestedItem[nestedParentKeyField])) {
|
|
46
|
+
toAddToAllParents.push(nestedItem); // TODO explain this odd case
|
|
47
|
+
return; // Avoids adding the nestedItem twice
|
|
48
|
+
}
|
|
49
|
+
const parentPrimaryKey = nestedItem[nestedParentKeyField]?.[parentPrimaryKeyField] ?? nestedItem[nestedParentKeyField];
|
|
50
|
+
const parentItem = parentsByPrimaryKey.get(parentPrimaryKey);
|
|
51
|
+
if (!parentItem) {
|
|
52
|
+
throw new Error(`Missing parentItem '${nestedItem[nestedParentKeyField]}' of '${parentCollectionName}' when merging o2m nested items`);
|
|
53
|
+
}
|
|
54
|
+
parentItem[parentRelationField].push(nestedItem);
|
|
55
|
+
});
|
|
18
56
|
for (const [index, parentItem] of parentItems.entries()) {
|
|
19
57
|
if (fieldAllowed === false || (isArray(fieldAllowed) && !fieldAllowed[index])) {
|
|
20
58
|
parentItem[nestedNode.fieldKey] = null;
|
|
21
59
|
continue;
|
|
22
60
|
}
|
|
23
|
-
|
|
24
|
-
parentItem[nestedNode.fieldKey] = [];
|
|
25
|
-
const itemChildren = nestedItems.filter((nestedItem) => {
|
|
26
|
-
if (nestedItem === null)
|
|
27
|
-
return false;
|
|
28
|
-
if (Array.isArray(nestedItem[nestedNode.relation.field]))
|
|
29
|
-
return true;
|
|
30
|
-
return (nestedItem[nestedNode.relation.field] ==
|
|
31
|
-
parentItem[schema.collections[nestedNode.relation.related_collection].primary] ||
|
|
32
|
-
nestedItem[nestedNode.relation.field]?.[schema.collections[nestedNode.relation.related_collection].primary] == parentItem[schema.collections[nestedNode.relation.related_collection].primary]);
|
|
33
|
-
});
|
|
34
|
-
parentItem[nestedNode.fieldKey].push(...itemChildren);
|
|
61
|
+
parentItem[parentRelationField].push(...toAddToAllParents);
|
|
35
62
|
const limit = nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT']);
|
|
36
63
|
if (nestedNode.query.page && nestedNode.query.page > 1) {
|
|
37
64
|
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(limit * (nestedNode.query.page - 1));
|
|
@@ -18,8 +18,8 @@ export async function fetchDynamicVariableContext(options, context) {
|
|
|
18
18
|
}
|
|
19
19
|
if (options.accountability.role && (permissionContext.$CURRENT_ROLE?.size ?? 0) > 0) {
|
|
20
20
|
contextData['$CURRENT_ROLE'] = await fetchContextData('$CURRENT_ROLE', permissionContext, { role: options.accountability.role }, async (fields) => {
|
|
21
|
-
const
|
|
22
|
-
return await
|
|
21
|
+
const rolesService = new RolesService(context);
|
|
22
|
+
return await rolesService.readOne(options.accountability.role, {
|
|
23
23
|
fields,
|
|
24
24
|
});
|
|
25
25
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Action } from '@directus/constants';
|
|
2
2
|
import { useEnv } from '@directus/env';
|
|
3
3
|
import { toBoolean } from '@directus/utils';
|
|
4
|
+
import { getHelpers } from '../database/helpers/index.js';
|
|
4
5
|
import getDatabase from '../database/index.js';
|
|
5
6
|
import { useLock } from '../lock/index.js';
|
|
6
7
|
import { useLogger } from '../logger/index.js';
|
|
@@ -31,6 +32,7 @@ export async function handleRetentionJob() {
|
|
|
31
32
|
const batch = Number(env['RETENTION_BATCH']);
|
|
32
33
|
const lockTime = await lock.get(retentionLockKey);
|
|
33
34
|
const now = Date.now();
|
|
35
|
+
const helpers = getHelpers(database);
|
|
34
36
|
if (lockTime && Number(lockTime) > now - retentionLockTimeout) {
|
|
35
37
|
// ensure only one connected process
|
|
36
38
|
return;
|
|
@@ -47,7 +49,7 @@ export async function handleRetentionJob() {
|
|
|
47
49
|
.queryBuilder()
|
|
48
50
|
.select(`${task.collection}.id`)
|
|
49
51
|
.from(task.collection)
|
|
50
|
-
.where('timestamp', '<', Date.now() - task.timeframe)
|
|
52
|
+
.where('timestamp', '<', helpers.date.parse(new Date(Date.now() - task.timeframe)))
|
|
51
53
|
.limit(batch);
|
|
52
54
|
if (task.where) {
|
|
53
55
|
subquery.where(...task.where);
|
|
@@ -56,7 +58,19 @@ export async function handleRetentionJob() {
|
|
|
56
58
|
subquery.join(...task.join);
|
|
57
59
|
}
|
|
58
60
|
try {
|
|
59
|
-
|
|
61
|
+
let records = [];
|
|
62
|
+
const isMySQL = helpers.schema.isOneOfClients(['mysql']);
|
|
63
|
+
// mysql/maria does not allow limit within a subquery
|
|
64
|
+
// https://dev.mysql.com/doc/refman/8.4/en/subquery-restrictions.html
|
|
65
|
+
if (isMySQL) {
|
|
66
|
+
records = await subquery.then((r) => r.map((r) => r.id));
|
|
67
|
+
if (records.length === 0) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
count = await database(task.collection)
|
|
72
|
+
.whereIn('id', isMySQL ? records : subquery)
|
|
73
|
+
.delete();
|
|
60
74
|
}
|
|
61
75
|
catch (error) {
|
|
62
76
|
logger.error(error, `Retention failed for Collection ${task.collection}`);
|
|
@@ -132,12 +132,12 @@ export class CollectionsService {
|
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
if (payload.meta) {
|
|
135
|
-
const
|
|
135
|
+
const collectionsItemsService = new ItemsService('directus_collections', {
|
|
136
136
|
knex: trx,
|
|
137
137
|
accountability: this.accountability,
|
|
138
138
|
schema: this.schema,
|
|
139
139
|
});
|
|
140
|
-
await
|
|
140
|
+
await collectionsItemsService.createOne({
|
|
141
141
|
...payload.meta,
|
|
142
142
|
collection: payload.collection,
|
|
143
143
|
}, {
|
|
@@ -210,13 +210,13 @@ export class CollectionsService {
|
|
|
210
210
|
*/
|
|
211
211
|
async readByQuery() {
|
|
212
212
|
const env = useEnv();
|
|
213
|
-
const
|
|
213
|
+
const collectionsItemsService = new ItemsService('directus_collections', {
|
|
214
214
|
knex: this.knex,
|
|
215
215
|
schema: this.schema,
|
|
216
216
|
accountability: this.accountability,
|
|
217
217
|
});
|
|
218
218
|
let tablesInDatabase = await this.schemaInspector.tableInfo();
|
|
219
|
-
let meta = (await
|
|
219
|
+
let meta = (await collectionsItemsService.readByQuery({
|
|
220
220
|
limit: -1,
|
|
221
221
|
}));
|
|
222
222
|
meta.push(...systemCollectionRows);
|
|
@@ -306,7 +306,7 @@ export class CollectionsService {
|
|
|
306
306
|
}
|
|
307
307
|
const nestedActionEvents = [];
|
|
308
308
|
try {
|
|
309
|
-
const
|
|
309
|
+
const collectionsItemsService = new ItemsService('directus_collections', {
|
|
310
310
|
knex: this.knex,
|
|
311
311
|
accountability: this.accountability,
|
|
312
312
|
schema: this.schema,
|
|
@@ -321,13 +321,13 @@ export class CollectionsService {
|
|
|
321
321
|
.where({ collection: collectionKey })
|
|
322
322
|
.first());
|
|
323
323
|
if (exists) {
|
|
324
|
-
await
|
|
324
|
+
await collectionsItemsService.updateOne(collectionKey, payload.meta, {
|
|
325
325
|
...opts,
|
|
326
326
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
327
327
|
});
|
|
328
328
|
}
|
|
329
329
|
else {
|
|
330
|
-
await
|
|
330
|
+
await collectionsItemsService.createOne({ ...payload.meta, collection: collectionKey }, {
|
|
331
331
|
...opts,
|
|
332
332
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
333
333
|
});
|
|
@@ -463,12 +463,12 @@ export class CollectionsService {
|
|
|
463
463
|
// Make sure this collection isn't used as a group in any other collections
|
|
464
464
|
await trx('directus_collections').update({ group: null }).where({ group: collectionKey });
|
|
465
465
|
if (collectionToBeDeleted.meta) {
|
|
466
|
-
const
|
|
466
|
+
const collectionsItemsService = new ItemsService('directus_collections', {
|
|
467
467
|
knex: trx,
|
|
468
468
|
accountability: this.accountability,
|
|
469
469
|
schema: this.schema,
|
|
470
470
|
});
|
|
471
|
-
await
|
|
471
|
+
await collectionsItemsService.deleteOne(collectionKey, {
|
|
472
472
|
bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
|
|
473
473
|
});
|
|
474
474
|
}
|
package/dist/services/files.js
CHANGED
|
@@ -133,11 +133,11 @@ export class FilesService extends ItemsService {
|
|
|
133
133
|
payload.uploaded_on = new Date().toISOString();
|
|
134
134
|
// We do this in a service without accountability. Even if you don't have update permissions to the file,
|
|
135
135
|
// we still want to be able to set the extracted values from the file on create
|
|
136
|
-
const
|
|
136
|
+
const sudoFilesItemsService = new ItemsService('directus_files', {
|
|
137
137
|
knex: this.knex,
|
|
138
138
|
schema: this.schema,
|
|
139
139
|
});
|
|
140
|
-
await
|
|
140
|
+
await sudoFilesItemsService.updateOne(primaryKey, { ...payload, ...metadata }, { emitEvents: false });
|
|
141
141
|
if (opts?.emitEvents !== false) {
|
|
142
142
|
emitter.emitAction('files.upload', {
|
|
143
143
|
payload,
|
package/dist/services/payload.js
CHANGED
|
@@ -144,13 +144,13 @@ export class PayloadService {
|
|
|
144
144
|
return fieldsInPayload.includes(name);
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
for (const record of processedPayload) {
|
|
148
|
+
for (const [name, field] of specialFields) {
|
|
149
149
|
const newValue = await this.processField(field, record, action, this.accountability);
|
|
150
150
|
if (newValue !== undefined)
|
|
151
151
|
record[name] = newValue;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
154
|
this.processGeometries(processedPayload, action);
|
|
155
155
|
this.processDates(processedPayload, action);
|
|
156
156
|
if (['create', 'update'].includes(action)) {
|
package/dist/services/roles.js
CHANGED
|
@@ -39,7 +39,7 @@ export class RolesService extends ItemsService {
|
|
|
39
39
|
accountability: this.accountability,
|
|
40
40
|
schema: this.schema,
|
|
41
41
|
};
|
|
42
|
-
const
|
|
42
|
+
const rolesItemsService = new ItemsService('directus_roles', options);
|
|
43
43
|
const rolesService = new RolesService(options);
|
|
44
44
|
const accessService = new AccessService(options);
|
|
45
45
|
const presetsService = new PresetsService(options);
|
|
@@ -62,7 +62,7 @@ export class RolesService extends ItemsService {
|
|
|
62
62
|
await rolesService.updateByQuery({
|
|
63
63
|
filter: { parent: { _in: keys } },
|
|
64
64
|
}, { parent: null });
|
|
65
|
-
await
|
|
65
|
+
await rolesItemsService.deleteMany(keys, opts);
|
|
66
66
|
});
|
|
67
67
|
// Since nested roles could be updated, clear caches
|
|
68
68
|
await this.clearCaches();
|
|
@@ -29,7 +29,7 @@ export class TusDataStore extends DataStore {
|
|
|
29
29
|
async create(upload) {
|
|
30
30
|
const logger = useLogger();
|
|
31
31
|
const knex = getDatabase();
|
|
32
|
-
const
|
|
32
|
+
const filesItemsService = new ItemsService('directus_files', {
|
|
33
33
|
accountability: this.accountability,
|
|
34
34
|
schema: this.schema,
|
|
35
35
|
knex,
|
|
@@ -78,7 +78,7 @@ export class TusDataStore extends DataStore {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
// If this is a new file upload, we need to generate a new primary key and DB record
|
|
81
|
-
const primaryKey = await
|
|
81
|
+
const primaryKey = await filesItemsService.createOne(fileData, { emitEvents: false });
|
|
82
82
|
// Set the file id, so it is available to be sent as a header on upload creation / resume
|
|
83
83
|
if (!upload.metadata['id']) {
|
|
84
84
|
upload.metadata['id'] = primaryKey;
|
|
@@ -92,17 +92,17 @@ export class TusDataStore extends DataStore {
|
|
|
92
92
|
// If this is a replacement, we'll write the file to a temp location first to ensure we don't overwrite the existing file if something goes wrong
|
|
93
93
|
upload = (await this.storageDriver.createChunkedUpload(fileData.filename_disk, upload));
|
|
94
94
|
fileData.tus_data = upload;
|
|
95
|
-
await
|
|
95
|
+
await filesItemsService.updateOne(primaryKey, fileData, { emitEvents: false });
|
|
96
96
|
return upload;
|
|
97
97
|
}
|
|
98
98
|
catch (err) {
|
|
99
99
|
logger.warn(`Couldn't create chunked upload for ${fileData.filename_disk}`);
|
|
100
100
|
logger.warn(err);
|
|
101
101
|
if (isReplacement) {
|
|
102
|
-
await
|
|
102
|
+
await filesItemsService.updateOne(primaryKey, { tus_id: null, tus_data: null }, { emitEvents: false });
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
105
|
-
await
|
|
105
|
+
await filesItemsService.deleteOne(primaryKey, { emitEvents: false });
|
|
106
106
|
}
|
|
107
107
|
throw ERRORS.UNKNOWN_ERROR;
|
|
108
108
|
}
|
|
@@ -111,12 +111,12 @@ export class TusDataStore extends DataStore {
|
|
|
111
111
|
const logger = useLogger();
|
|
112
112
|
const fileData = await this.getFileById(tus_id);
|
|
113
113
|
const filePath = fileData.filename_disk;
|
|
114
|
-
const
|
|
114
|
+
const sudoFilesItemsService = new ItemsService('directus_files', {
|
|
115
115
|
schema: this.schema,
|
|
116
116
|
});
|
|
117
117
|
try {
|
|
118
118
|
const newOffset = await this.storageDriver.writeChunk(filePath, readable, offset, fileData.tus_data);
|
|
119
|
-
await
|
|
119
|
+
await sudoFilesItemsService.updateOne(fileData.id, {
|
|
120
120
|
tus_data: {
|
|
121
121
|
...fileData.tus_data,
|
|
122
122
|
offset: newOffset,
|
|
@@ -134,7 +134,7 @@ export class TusDataStore extends DataStore {
|
|
|
134
134
|
// If the file is a replacement, delete the old files, and upgrade the temp file
|
|
135
135
|
if (isReplacement === true) {
|
|
136
136
|
const replaceId = fileData.tus_data['metadata']['replace_id'];
|
|
137
|
-
const replaceData = await
|
|
137
|
+
const replaceData = await sudoFilesItemsService.readOne(replaceId, { fields: ['filename_disk'] });
|
|
138
138
|
// delete the previously saved file and thumbnails to ensure they're generated fresh
|
|
139
139
|
for await (const partPath of this.storageDriver.list(replaceId)) {
|
|
140
140
|
await this.storageDriver.delete(partPath);
|
|
@@ -154,20 +154,20 @@ export class TusDataStore extends DataStore {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
async remove(tus_id) {
|
|
157
|
-
const
|
|
157
|
+
const sudoFilesItemsService = new ItemsService('directus_files', {
|
|
158
158
|
schema: this.schema,
|
|
159
159
|
});
|
|
160
160
|
const fileData = await this.getFileById(tus_id);
|
|
161
161
|
await this.storageDriver.deleteChunkedUpload(fileData.filename_disk, fileData.tus_data);
|
|
162
|
-
await
|
|
162
|
+
await sudoFilesItemsService.deleteOne(fileData.id);
|
|
163
163
|
}
|
|
164
164
|
async deleteExpired() {
|
|
165
|
-
const
|
|
165
|
+
const sudoFilesItemsService = new ItemsService('directus_files', {
|
|
166
166
|
schema: this.schema,
|
|
167
167
|
});
|
|
168
168
|
const now = new Date();
|
|
169
169
|
const toDelete = [];
|
|
170
|
-
const uploadFiles = await
|
|
170
|
+
const uploadFiles = await sudoFilesItemsService.readByQuery({
|
|
171
171
|
fields: ['modified_on', 'tus_id', 'tus_data'],
|
|
172
172
|
filter: { tus_id: { _nnull: true } },
|
|
173
173
|
});
|
|
@@ -197,10 +197,10 @@ export class TusDataStore extends DataStore {
|
|
|
197
197
|
return new Upload(fileData.tus_data);
|
|
198
198
|
}
|
|
199
199
|
async getFileById(tus_id) {
|
|
200
|
-
const
|
|
200
|
+
const sudoFilesItemsService = new ItemsService('directus_files', {
|
|
201
201
|
schema: this.schema,
|
|
202
202
|
});
|
|
203
|
-
const results = await
|
|
203
|
+
const results = await sudoFilesItemsService.readByQuery({
|
|
204
204
|
filter: {
|
|
205
205
|
tus_id: { _eq: tus_id },
|
|
206
206
|
storage: { _eq: this.location },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { flushCaches } from '../cache.js';
|
|
2
2
|
import getDatabase from '../database/index.js';
|
|
3
3
|
import { applyDiff } from './apply-diff.js';
|
|
4
4
|
import { getSchema } from './get-schema.js';
|
|
5
|
-
import { getSnapshot } from './get-snapshot.js';
|
|
6
5
|
import { getSnapshotDiff } from './get-snapshot-diff.js';
|
|
6
|
+
import { getSnapshot } from './get-snapshot.js';
|
|
7
7
|
export async function applySnapshot(snapshot, options) {
|
|
8
8
|
const database = options?.database ?? getDatabase();
|
|
9
9
|
const schema = options?.schema ?? (await getSchema({ database, bypassCache: true }));
|
|
10
|
-
const { systemCache } = getCache();
|
|
11
10
|
const current = options?.current ?? (await getSnapshot({ database, schema }));
|
|
12
11
|
const snapshotDiff = options?.diff ?? getSnapshotDiff(current, snapshot);
|
|
13
12
|
await applyDiff(current, snapshotDiff, { database, schema });
|
|
14
|
-
await
|
|
13
|
+
await flushCaches();
|
|
15
14
|
}
|
|
@@ -10,7 +10,7 @@ export function startWebSocketHandlers() {
|
|
|
10
10
|
const restEnabled = toBoolean(env['WEBSOCKETS_REST_ENABLED']);
|
|
11
11
|
const graphqlEnabled = toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED']);
|
|
12
12
|
const logsEnabled = toBoolean(env['WEBSOCKETS_LOGS_ENABLED']);
|
|
13
|
-
if (heartbeatEnabled) {
|
|
13
|
+
if (restEnabled && heartbeatEnabled) {
|
|
14
14
|
new HeartbeatHandler();
|
|
15
15
|
}
|
|
16
16
|
if (restEnabled || graphqlEnabled) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "23.
|
|
3
|
+
"version": "23.3.0",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -149,29 +149,29 @@
|
|
|
149
149
|
"ws": "8.18.0",
|
|
150
150
|
"zod": "3.23.8",
|
|
151
151
|
"zod-validation-error": "3.4.0",
|
|
152
|
-
"@directus/app": "13.3.
|
|
152
|
+
"@directus/app": "13.3.7",
|
|
153
153
|
"@directus/constants": "12.0.1",
|
|
154
|
-
"@directus/errors": "1.0.1",
|
|
155
154
|
"@directus/env": "4.1.0",
|
|
156
|
-
"@directus/extensions": "2.0.6",
|
|
157
|
-
"@directus/format-title": "11.0.0",
|
|
158
155
|
"@directus/extensions-registry": "2.0.6",
|
|
156
|
+
"@directus/errors": "1.0.1",
|
|
159
157
|
"@directus/extensions-sdk": "12.1.4",
|
|
158
|
+
"@directus/format-title": "11.0.0",
|
|
160
159
|
"@directus/memory": "2.0.6",
|
|
160
|
+
"@directus/extensions": "2.0.6",
|
|
161
161
|
"@directus/pressure": "2.0.5",
|
|
162
162
|
"@directus/schema": "12.1.1",
|
|
163
163
|
"@directus/specs": "11.1.0",
|
|
164
164
|
"@directus/storage": "11.0.1",
|
|
165
165
|
"@directus/storage-driver-azure": "11.1.2",
|
|
166
|
-
"@directus/storage-driver-cloudinary": "11.1.2",
|
|
167
166
|
"@directus/storage-driver-gcs": "11.1.2",
|
|
168
|
-
"@directus/storage-driver-
|
|
167
|
+
"@directus/storage-driver-cloudinary": "11.1.2",
|
|
169
168
|
"@directus/storage-driver-local": "11.0.1",
|
|
170
|
-
"@directus/
|
|
169
|
+
"@directus/storage-driver-supabase": "2.1.2",
|
|
171
170
|
"@directus/storage-driver-s3": "11.0.5",
|
|
171
|
+
"@directus/system-data": "2.1.2",
|
|
172
172
|
"@directus/utils": "12.0.5",
|
|
173
173
|
"@directus/validation": "1.0.5",
|
|
174
|
-
"directus": "11.3.
|
|
174
|
+
"directus": "11.3.3"
|
|
175
175
|
},
|
|
176
176
|
"devDependencies": {
|
|
177
177
|
"@ngneat/falso": "7.2.0",
|
|
@@ -213,9 +213,9 @@
|
|
|
213
213
|
"knex-mock-client": "3.0.2",
|
|
214
214
|
"typescript": "5.6.3",
|
|
215
215
|
"vitest": "2.1.2",
|
|
216
|
-
"@directus/tsconfig": "2.0.0",
|
|
217
216
|
"@directus/random": "1.0.0",
|
|
218
|
-
"@directus/types": "12.2.2"
|
|
217
|
+
"@directus/types": "12.2.2",
|
|
218
|
+
"@directus/tsconfig": "2.0.0"
|
|
219
219
|
},
|
|
220
220
|
"optionalDependencies": {
|
|
221
221
|
"@keyv/redis": "3.0.1",
|