@directus/api 13.1.0-beta.0 → 13.1.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/__utils__/snapshots.js +0 -9
- package/dist/app.js +0 -2
- package/dist/controllers/files.js +1 -1
- package/dist/database/helpers/index.d.ts +2 -0
- package/dist/database/helpers/index.js +2 -0
- package/dist/database/helpers/sequence/dialects/default.d.ts +3 -0
- package/dist/database/helpers/sequence/dialects/default.js +3 -0
- package/dist/database/helpers/sequence/dialects/postgres.d.ts +9 -0
- package/dist/database/helpers/sequence/dialects/postgres.js +10 -0
- package/dist/database/helpers/sequence/index.d.ts +7 -0
- package/dist/database/helpers/sequence/index.js +7 -0
- package/dist/database/helpers/sequence/types.d.ts +5 -0
- package/dist/database/helpers/sequence/types.js +6 -0
- package/dist/database/index.js +8 -0
- package/dist/database/migrations/20230721A-require-shares-fields.js +45 -16
- package/dist/database/system-data/fields/collections.yaml +0 -19
- package/dist/env.d.ts +1 -1
- package/dist/env.js +18 -15
- package/dist/middleware/respond.js +0 -20
- package/dist/services/activity.js +4 -3
- package/dist/services/assets.js +17 -1
- package/dist/services/files.js +58 -1
- package/dist/services/import-export.js +16 -0
- package/dist/services/items.js +28 -3
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +19 -0
- package/dist/types/collection.d.ts +0 -1
- package/dist/types/items.d.ts +1 -0
- package/dist/utils/sanitize-query.js +0 -3
- package/dist/utils/validate-query.js +0 -1
- package/dist/websocket/controllers/base.d.ts +1 -0
- package/dist/websocket/controllers/base.js +16 -0
- package/package.json +15 -15
- package/dist/controllers/branches.d.ts +0 -2
- package/dist/controllers/branches.js +0 -190
- package/dist/database/migrations/20230823A-add-content-versioning.d.ts +0 -3
- package/dist/database/migrations/20230823A-add-content-versioning.js +0 -26
- package/dist/database/system-data/fields/branches.yaml +0 -19
- package/dist/services/branches.d.ts +0 -25
- package/dist/services/branches.js +0 -205
package/dist/services/items.js
CHANGED
|
@@ -116,8 +116,20 @@ export class ItemsService {
|
|
|
116
116
|
const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
|
|
117
117
|
const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
|
|
118
118
|
const payloadWithTypeCasting = await payloadService.processValues('create', payloadWithoutAliases);
|
|
119
|
-
//
|
|
119
|
+
// The primary key can already exist in the payload.
|
|
120
|
+
// In case of manual string / UUID primary keys it's always provided at this point.
|
|
121
|
+
// In case of an integer primary key, it might be provided as the user can specify the value manually.
|
|
120
122
|
let primaryKey = payloadWithTypeCasting[primaryKeyField];
|
|
123
|
+
// If a PK of type number was provided, although the PK is set the auto_increment,
|
|
124
|
+
// depending on the database, the sequence might need to be reset to protect future PK collisions.
|
|
125
|
+
let autoIncrementSequenceNeedsToBeReset = false;
|
|
126
|
+
const pkField = this.schema.collections[this.collection].fields[primaryKeyField];
|
|
127
|
+
if (primaryKey &&
|
|
128
|
+
!opts.bypassAutoIncrementSequenceReset &&
|
|
129
|
+
pkField.type === 'integer' &&
|
|
130
|
+
pkField.defaultValue === 'AUTO_INCREMENT') {
|
|
131
|
+
autoIncrementSequenceNeedsToBeReset = true;
|
|
132
|
+
}
|
|
121
133
|
try {
|
|
122
134
|
const result = await trx
|
|
123
135
|
.insert(payloadWithoutAliases)
|
|
@@ -125,7 +137,7 @@ export class ItemsService {
|
|
|
125
137
|
.returning(primaryKeyField)
|
|
126
138
|
.then((result) => result[0]);
|
|
127
139
|
const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
|
|
128
|
-
if (
|
|
140
|
+
if (pkField.type === 'uuid') {
|
|
129
141
|
primaryKey = getHelpers(trx).schema.formatUUID(primaryKey ?? returnedKey);
|
|
130
142
|
}
|
|
131
143
|
else {
|
|
@@ -146,6 +158,8 @@ export class ItemsService {
|
|
|
146
158
|
// to read from it
|
|
147
159
|
payload[primaryKeyField] = primaryKey;
|
|
148
160
|
}
|
|
161
|
+
// At this point, the primary key is guaranteed to be set.
|
|
162
|
+
primaryKey = primaryKey;
|
|
149
163
|
const { revisions: revisionsO2M, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payloadWithPresets, primaryKey, opts);
|
|
150
164
|
nestedActionEvents.push(...nestedActionEventsM2O);
|
|
151
165
|
nestedActionEvents.push(...nestedActionEventsA2O);
|
|
@@ -189,6 +203,9 @@ export class ItemsService {
|
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
205
|
}
|
|
206
|
+
if (autoIncrementSequenceNeedsToBeReset) {
|
|
207
|
+
await getHelpers(trx).sequence.resetAutoIncrementSequence(this.collection, primaryKeyField);
|
|
208
|
+
}
|
|
192
209
|
return primaryKey;
|
|
193
210
|
});
|
|
194
211
|
if (opts.emitEvents !== false) {
|
|
@@ -241,12 +258,20 @@ export class ItemsService {
|
|
|
241
258
|
});
|
|
242
259
|
const primaryKeys = [];
|
|
243
260
|
const nestedActionEvents = [];
|
|
244
|
-
|
|
261
|
+
const pkField = this.schema.collections[this.collection].primary;
|
|
262
|
+
for (const [index, payload] of data.entries()) {
|
|
263
|
+
let bypassAutoIncrementSequenceReset = true;
|
|
264
|
+
// the auto_increment sequence needs to be reset if the current item contains a manual PK and
|
|
265
|
+
// if it's the last item of the batch or if the next item doesn't include a PK and hence one needs to be generated
|
|
266
|
+
if (payload[pkField] && (index === data.length - 1 || !data[index + 1]?.[pkField])) {
|
|
267
|
+
bypassAutoIncrementSequenceReset = false;
|
|
268
|
+
}
|
|
245
269
|
const primaryKey = await service.createOne(payload, {
|
|
246
270
|
...(opts || {}),
|
|
247
271
|
autoPurgeCache: false,
|
|
248
272
|
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
|
249
273
|
mutationTracker: opts.mutationTracker,
|
|
274
|
+
bypassAutoIncrementSequenceReset,
|
|
250
275
|
});
|
|
251
276
|
primaryKeys.push(primaryKey);
|
|
252
277
|
}
|
package/dist/services/users.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export declare class UsersService extends ItemsService {
|
|
|
26
26
|
* Create url for inviting users
|
|
27
27
|
*/
|
|
28
28
|
private inviteUrl;
|
|
29
|
+
/**
|
|
30
|
+
* Validate array of emails. Intended to be used with create/update users
|
|
31
|
+
*/
|
|
32
|
+
private validateEmail;
|
|
29
33
|
/**
|
|
30
34
|
* Create a new user
|
|
31
35
|
*/
|
package/dist/services/users.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSimpleHash, toArray } from '@directus/utils';
|
|
2
2
|
import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
|
|
3
|
+
import Joi from 'joi';
|
|
3
4
|
import jwt from 'jsonwebtoken';
|
|
4
5
|
import { cloneDeep, isEmpty } from 'lodash-es';
|
|
5
6
|
import { performance } from 'perf_hooks';
|
|
@@ -130,6 +131,22 @@ export class UsersService extends ItemsService {
|
|
|
130
131
|
inviteURL.setQuery('token', token);
|
|
131
132
|
return inviteURL.toString();
|
|
132
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Validate array of emails. Intended to be used with create/update users
|
|
136
|
+
*/
|
|
137
|
+
validateEmail(input) {
|
|
138
|
+
const emails = Array.isArray(input) ? input : [input];
|
|
139
|
+
const schema = Joi.string().email().required();
|
|
140
|
+
for (const email of emails) {
|
|
141
|
+
const { error } = schema.validate(email);
|
|
142
|
+
if (error) {
|
|
143
|
+
throw new FailedValidationError({
|
|
144
|
+
field: 'email',
|
|
145
|
+
type: 'email',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
133
150
|
/**
|
|
134
151
|
* Create a new user
|
|
135
152
|
*/
|
|
@@ -145,6 +162,7 @@ export class UsersService extends ItemsService {
|
|
|
145
162
|
const passwords = data['map']((payload) => payload['password']).filter((password) => password);
|
|
146
163
|
try {
|
|
147
164
|
if (emails.length) {
|
|
165
|
+
this.validateEmail(emails);
|
|
148
166
|
await this.checkUniqueEmails(emails);
|
|
149
167
|
}
|
|
150
168
|
if (passwords.length) {
|
|
@@ -212,6 +230,7 @@ export class UsersService extends ItemsService {
|
|
|
212
230
|
field: 'email',
|
|
213
231
|
});
|
|
214
232
|
}
|
|
233
|
+
this.validateEmail(data['email']);
|
|
215
234
|
await this.checkUniqueEmails([data['email']], keys[0]);
|
|
216
235
|
}
|
|
217
236
|
if (data['password']) {
|
|
@@ -7,7 +7,6 @@ export type CollectionMeta = {
|
|
|
7
7
|
singleton: boolean;
|
|
8
8
|
icon: string | null;
|
|
9
9
|
translations: Record<string, string>;
|
|
10
|
-
branches_enabled: boolean;
|
|
11
10
|
item_duplication_fields: string[] | null;
|
|
12
11
|
accountability: 'all' | 'accountability' | null;
|
|
13
12
|
group: string | null;
|
package/dist/types/items.d.ts
CHANGED
|
@@ -46,9 +46,6 @@ export function sanitizeQuery(rawQuery, accountability) {
|
|
|
46
46
|
if (rawQuery['search'] && typeof rawQuery['search'] === 'string') {
|
|
47
47
|
query.search = rawQuery['search'];
|
|
48
48
|
}
|
|
49
|
-
if (rawQuery['branch']) {
|
|
50
|
-
query.branch = rawQuery['branch'];
|
|
51
|
-
}
|
|
52
49
|
if (rawQuery['export']) {
|
|
53
50
|
query.export = rawQuery['export'];
|
|
54
51
|
}
|
|
@@ -17,7 +17,6 @@ const querySchema = Joi.object({
|
|
|
17
17
|
meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
|
|
18
18
|
search: Joi.string(),
|
|
19
19
|
export: Joi.string().valid('csv', 'json', 'xml', 'yaml'),
|
|
20
|
-
branch: Joi.string(),
|
|
21
20
|
aggregate: Joi.object(),
|
|
22
21
|
deep: Joi.object(),
|
|
23
22
|
alias: Joi.object(),
|
|
@@ -30,6 +30,7 @@ export default abstract class SocketController {
|
|
|
30
30
|
maxConnections: number;
|
|
31
31
|
};
|
|
32
32
|
protected getRateLimiter(): RateLimiterAbstract | null;
|
|
33
|
+
private catchInvalidMessages;
|
|
33
34
|
protected handleUpgrade(request: IncomingMessage, socket: internal.Duplex, head: Buffer): Promise<void>;
|
|
34
35
|
protected handleStrictUpgrade({ request, socket, head }: UpgradeContext, query: ParsedUrlQuery): Promise<void>;
|
|
35
36
|
protected handleHandshakeUpgrade({ request, socket, head }: UpgradeContext): Promise<void>;
|
|
@@ -67,6 +67,19 @@ export default class SocketController {
|
|
|
67
67
|
}
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
|
+
catchInvalidMessages(ws) {
|
|
71
|
+
/**
|
|
72
|
+
* This fix was done to prevent the API from crashing on receiving invalid WebSocket frames
|
|
73
|
+
* https://github.com/directus/directus/security/advisories/GHSA-hmgw-9jrg-hf2m
|
|
74
|
+
* https://github.com/websockets/ws/issues/2098
|
|
75
|
+
*/
|
|
76
|
+
// @ts-ignore <- required because "_socket" is not typed on WS
|
|
77
|
+
ws._socket.prependListener('data', (data) => data.toString());
|
|
78
|
+
ws.on('error', (error) => {
|
|
79
|
+
if (error.message)
|
|
80
|
+
logger.debug(error.message);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
70
83
|
async handleUpgrade(request, socket, head) {
|
|
71
84
|
const { pathname, query } = parse(request.url, true);
|
|
72
85
|
if (pathname !== this.endpoint)
|
|
@@ -87,6 +100,7 @@ export default class SocketController {
|
|
|
87
100
|
return;
|
|
88
101
|
}
|
|
89
102
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
103
|
+
this.catchInvalidMessages(ws);
|
|
90
104
|
const state = { accountability: null, expires_at: null };
|
|
91
105
|
this.server.emit('connection', ws, state);
|
|
92
106
|
});
|
|
@@ -109,12 +123,14 @@ export default class SocketController {
|
|
|
109
123
|
return;
|
|
110
124
|
}
|
|
111
125
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
126
|
+
this.catchInvalidMessages(ws);
|
|
112
127
|
const state = { accountability, expires_at };
|
|
113
128
|
this.server.emit('connection', ws, state);
|
|
114
129
|
});
|
|
115
130
|
}
|
|
116
131
|
async handleHandshakeUpgrade({ request, socket, head }) {
|
|
117
132
|
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
133
|
+
this.catchInvalidMessages(ws);
|
|
118
134
|
try {
|
|
119
135
|
const payload = await waitForAnyMessage(ws, this.authentication.timeout);
|
|
120
136
|
if (getMessageType(payload) !== 'auth')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@directus/api",
|
|
3
|
-
"version": "13.1.
|
|
3
|
+
"version": "13.1.1",
|
|
4
4
|
"description": "Directus is a real-time API and App dashboard for managing SQL database content",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"directus",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
"rollup": "3.22.0",
|
|
132
132
|
"samlify": "2.8.10",
|
|
133
133
|
"sanitize-html": "2.10.0",
|
|
134
|
-
"sharp": "0.32.
|
|
134
|
+
"sharp": "0.32.5",
|
|
135
135
|
"snappy": "7.2.2",
|
|
136
136
|
"stream-json": "1.7.5",
|
|
137
137
|
"strip-bom-stream": "5.0.0",
|
|
@@ -143,22 +143,22 @@
|
|
|
143
143
|
"ws": "8.12.1",
|
|
144
144
|
"zod": "3.21.4",
|
|
145
145
|
"zod-validation-error": "1.0.1",
|
|
146
|
-
"@directus/app": "10.
|
|
147
|
-
"@directus/constants": "10.2.
|
|
146
|
+
"@directus/app": "10.9.0",
|
|
147
|
+
"@directus/constants": "10.2.3",
|
|
148
148
|
"@directus/errors": "0.0.2",
|
|
149
|
-
"@directus/extensions-sdk": "10.1.
|
|
150
|
-
"@directus/pressure": "1.0.
|
|
149
|
+
"@directus/extensions-sdk": "10.1.11",
|
|
150
|
+
"@directus/pressure": "1.0.10",
|
|
151
151
|
"@directus/schema": "10.0.2",
|
|
152
152
|
"@directus/specs": "10.2.0",
|
|
153
153
|
"@directus/storage": "10.0.5",
|
|
154
|
-
"@directus/storage-driver-azure": "10.0.
|
|
155
|
-
"@directus/storage-driver-cloudinary": "10.0.
|
|
156
|
-
"@directus/storage-driver-gcs": "10.0.
|
|
157
|
-
"@directus/storage-driver-local": "10.0.
|
|
158
|
-
"@directus/storage-driver-s3": "10.0.
|
|
159
|
-
"@directus/storage-driver-supabase": "1.0.
|
|
160
|
-
"@directus/utils": "10.0.
|
|
161
|
-
"@directus/validation": "0.0.
|
|
154
|
+
"@directus/storage-driver-azure": "10.0.11",
|
|
155
|
+
"@directus/storage-driver-cloudinary": "10.0.11",
|
|
156
|
+
"@directus/storage-driver-gcs": "10.0.11",
|
|
157
|
+
"@directus/storage-driver-local": "10.0.11",
|
|
158
|
+
"@directus/storage-driver-s3": "10.0.11",
|
|
159
|
+
"@directus/storage-driver-supabase": "1.0.3",
|
|
160
|
+
"@directus/utils": "10.0.11",
|
|
161
|
+
"@directus/validation": "0.0.6"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@ngneat/falso": "6.4.0",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"vitest": "0.31.1",
|
|
208
208
|
"@directus/random": "0.2.2",
|
|
209
209
|
"@directus/tsconfig": "1.0.0",
|
|
210
|
-
"@directus/types": "10.1.6
|
|
210
|
+
"@directus/types": "10.1.6"
|
|
211
211
|
},
|
|
212
212
|
"optionalDependencies": {
|
|
213
213
|
"@keyv/redis": "2.5.8",
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { isDirectusError } from '@directus/errors';
|
|
2
|
-
import express from 'express';
|
|
3
|
-
import { assign } from 'lodash-es';
|
|
4
|
-
import { ErrorCode, InvalidPayloadError } from '../errors/index.js';
|
|
5
|
-
import { respond } from '../middleware/respond.js';
|
|
6
|
-
import useCollection from '../middleware/use-collection.js';
|
|
7
|
-
import { validateBatch } from '../middleware/validate-batch.js';
|
|
8
|
-
import { BranchesService } from '../services/branches.js';
|
|
9
|
-
import { MetaService } from '../services/meta.js';
|
|
10
|
-
import asyncHandler from '../utils/async-handler.js';
|
|
11
|
-
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
12
|
-
const router = express.Router();
|
|
13
|
-
router.use(useCollection('directus_branches'));
|
|
14
|
-
router.post('/', asyncHandler(async (req, res, next) => {
|
|
15
|
-
const service = new BranchesService({
|
|
16
|
-
accountability: req.accountability,
|
|
17
|
-
schema: req.schema,
|
|
18
|
-
});
|
|
19
|
-
const savedKeys = [];
|
|
20
|
-
if (Array.isArray(req.body)) {
|
|
21
|
-
const keys = await service.createMany(req.body);
|
|
22
|
-
savedKeys.push(...keys);
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
const primaryKey = await service.createOne(req.body);
|
|
26
|
-
savedKeys.push(primaryKey);
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
if (Array.isArray(req.body)) {
|
|
30
|
-
const records = await service.readMany(savedKeys, req.sanitizedQuery);
|
|
31
|
-
res.locals['payload'] = { data: records };
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
const record = await service.readOne(savedKeys[0], req.sanitizedQuery);
|
|
35
|
-
res.locals['payload'] = { data: record };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
if (isDirectusError(error, ErrorCode.Forbidden)) {
|
|
40
|
-
return next();
|
|
41
|
-
}
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
return next();
|
|
45
|
-
}), respond);
|
|
46
|
-
const readHandler = asyncHandler(async (req, res, next) => {
|
|
47
|
-
const service = new BranchesService({
|
|
48
|
-
accountability: req.accountability,
|
|
49
|
-
schema: req.schema,
|
|
50
|
-
});
|
|
51
|
-
const metaService = new MetaService({
|
|
52
|
-
accountability: req.accountability,
|
|
53
|
-
schema: req.schema,
|
|
54
|
-
});
|
|
55
|
-
let result;
|
|
56
|
-
if (req.singleton) {
|
|
57
|
-
result = await service.readSingleton(req.sanitizedQuery);
|
|
58
|
-
}
|
|
59
|
-
else if (req.body.keys) {
|
|
60
|
-
result = await service.readMany(req.body.keys, req.sanitizedQuery);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
result = await service.readByQuery(req.sanitizedQuery);
|
|
64
|
-
}
|
|
65
|
-
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
|
|
66
|
-
res.locals['payload'] = { data: result, meta };
|
|
67
|
-
return next();
|
|
68
|
-
});
|
|
69
|
-
router.get('/', validateBatch('read'), readHandler, respond);
|
|
70
|
-
router.search('/', validateBatch('read'), readHandler, respond);
|
|
71
|
-
router.get('/:pk', asyncHandler(async (req, res, next) => {
|
|
72
|
-
const service = new BranchesService({
|
|
73
|
-
accountability: req.accountability,
|
|
74
|
-
schema: req.schema,
|
|
75
|
-
});
|
|
76
|
-
const record = await service.readOne(req.params['pk'], req.sanitizedQuery);
|
|
77
|
-
res.locals['payload'] = { data: record || null };
|
|
78
|
-
return next();
|
|
79
|
-
}), respond);
|
|
80
|
-
router.patch('/', validateBatch('update'), asyncHandler(async (req, res, next) => {
|
|
81
|
-
const service = new BranchesService({
|
|
82
|
-
accountability: req.accountability,
|
|
83
|
-
schema: req.schema,
|
|
84
|
-
});
|
|
85
|
-
let keys = [];
|
|
86
|
-
if (Array.isArray(req.body)) {
|
|
87
|
-
keys = await service.updateBatch(req.body);
|
|
88
|
-
}
|
|
89
|
-
else if (req.body.keys) {
|
|
90
|
-
keys = await service.updateMany(req.body.keys, req.body.data);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
94
|
-
keys = await service.updateByQuery(sanitizedQuery, req.body.data);
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
const result = await service.readMany(keys, req.sanitizedQuery);
|
|
98
|
-
res.locals['payload'] = { data: result || null };
|
|
99
|
-
}
|
|
100
|
-
catch (error) {
|
|
101
|
-
if (isDirectusError(error, ErrorCode.Forbidden)) {
|
|
102
|
-
return next();
|
|
103
|
-
}
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
return next();
|
|
107
|
-
}), respond);
|
|
108
|
-
router.patch('/:pk', asyncHandler(async (req, res, next) => {
|
|
109
|
-
const service = new BranchesService({
|
|
110
|
-
accountability: req.accountability,
|
|
111
|
-
schema: req.schema,
|
|
112
|
-
});
|
|
113
|
-
const primaryKey = await service.updateOne(req.params['pk'], req.body);
|
|
114
|
-
try {
|
|
115
|
-
const record = await service.readOne(primaryKey, req.sanitizedQuery);
|
|
116
|
-
res.locals['payload'] = { data: record || null };
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
if (isDirectusError(error, ErrorCode.Forbidden)) {
|
|
120
|
-
return next();
|
|
121
|
-
}
|
|
122
|
-
throw error;
|
|
123
|
-
}
|
|
124
|
-
return next();
|
|
125
|
-
}), respond);
|
|
126
|
-
router.delete('/', validateBatch('delete'), asyncHandler(async (req, _res, next) => {
|
|
127
|
-
const service = new BranchesService({
|
|
128
|
-
accountability: req.accountability,
|
|
129
|
-
schema: req.schema,
|
|
130
|
-
});
|
|
131
|
-
if (Array.isArray(req.body)) {
|
|
132
|
-
await service.deleteMany(req.body);
|
|
133
|
-
}
|
|
134
|
-
else if (req.body.keys) {
|
|
135
|
-
await service.deleteMany(req.body.keys);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
const sanitizedQuery = sanitizeQuery(req.body.query, req.accountability);
|
|
139
|
-
await service.deleteByQuery(sanitizedQuery);
|
|
140
|
-
}
|
|
141
|
-
return next();
|
|
142
|
-
}), respond);
|
|
143
|
-
router.delete('/:pk', asyncHandler(async (req, _res, next) => {
|
|
144
|
-
const service = new BranchesService({
|
|
145
|
-
accountability: req.accountability,
|
|
146
|
-
schema: req.schema,
|
|
147
|
-
});
|
|
148
|
-
await service.deleteOne(req.params['pk']);
|
|
149
|
-
return next();
|
|
150
|
-
}), respond);
|
|
151
|
-
router.get('/:pk/compare', asyncHandler(async (req, res, next) => {
|
|
152
|
-
const service = new BranchesService({
|
|
153
|
-
accountability: req.accountability,
|
|
154
|
-
schema: req.schema,
|
|
155
|
-
});
|
|
156
|
-
const branch = await service.readOne(req.params['pk']);
|
|
157
|
-
const { outdated, mainHash } = await service.verifyHash(branch['collection'], branch['item'], branch['hash']);
|
|
158
|
-
const commits = await service.getBranchCommits(branch['id']);
|
|
159
|
-
const current = assign({}, ...commits);
|
|
160
|
-
const fields = Object.keys(current);
|
|
161
|
-
const mainBranchItem = await service.getMainBranchItem(branch['collection'], branch['item'], fields.length > 0 ? { fields } : undefined);
|
|
162
|
-
res.locals['payload'] = { data: { outdated, mainHash, current, main: mainBranchItem } };
|
|
163
|
-
return next();
|
|
164
|
-
}), respond);
|
|
165
|
-
router.post('/:pk/commit', asyncHandler(async (req, res, next) => {
|
|
166
|
-
const service = new BranchesService({
|
|
167
|
-
accountability: req.accountability,
|
|
168
|
-
schema: req.schema,
|
|
169
|
-
});
|
|
170
|
-
const branch = await service.readOne(req.params['pk']);
|
|
171
|
-
const mainBranchItem = await service.getMainBranchItem(branch['collection'], branch['item']);
|
|
172
|
-
await service.commit(req.params['pk'], req.body);
|
|
173
|
-
const commits = await service.getBranchCommits(req.params['pk']);
|
|
174
|
-
const result = assign(mainBranchItem, ...commits);
|
|
175
|
-
res.locals['payload'] = { data: result || null };
|
|
176
|
-
return next();
|
|
177
|
-
}), respond);
|
|
178
|
-
router.post('/:pk/merge', asyncHandler(async (req, res, next) => {
|
|
179
|
-
if (typeof req.body.mainHash !== 'string') {
|
|
180
|
-
throw new InvalidPayloadError({ reason: `"mainHash" field is required` });
|
|
181
|
-
}
|
|
182
|
-
const service = new BranchesService({
|
|
183
|
-
accountability: req.accountability,
|
|
184
|
-
schema: req.schema,
|
|
185
|
-
});
|
|
186
|
-
const updatedItemKey = await service.merge(req.params['pk'], req.body.mainHash, req.body?.['fields']);
|
|
187
|
-
res.locals['payload'] = { data: updatedItemKey || null };
|
|
188
|
-
return next();
|
|
189
|
-
}), respond);
|
|
190
|
-
export default router;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export async function up(knex) {
|
|
2
|
-
await knex.schema.createTable('directus_branches', (table) => {
|
|
3
|
-
table.uuid('id').primary().notNullable();
|
|
4
|
-
table.string('name').notNullable();
|
|
5
|
-
table.string('collection', 64).references('collection').inTable('directus_collections').onDelete('CASCADE');
|
|
6
|
-
table.string('item');
|
|
7
|
-
table.string('hash').notNullable();
|
|
8
|
-
table.timestamp('date_created').defaultTo(knex.fn.now());
|
|
9
|
-
table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL');
|
|
10
|
-
});
|
|
11
|
-
await knex.schema.alterTable('directus_collections', (table) => {
|
|
12
|
-
table.boolean('branches_enabled').notNullable().defaultTo(false);
|
|
13
|
-
});
|
|
14
|
-
await knex.schema.alterTable('directus_revisions', (table) => {
|
|
15
|
-
table.uuid('branch').references('id').inTable('directus_branches').onDelete('CASCADE');
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
export async function down(knex) {
|
|
19
|
-
await knex.schema.dropTable('directus_branches');
|
|
20
|
-
await knex.schema.alterTable('directus_collections', (table) => {
|
|
21
|
-
table.dropColumn('branches_enabled');
|
|
22
|
-
});
|
|
23
|
-
await knex.schema.alterTable('directus_revisions', (table) => {
|
|
24
|
-
table.dropColumn('branch');
|
|
25
|
-
});
|
|
26
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
table: directus_branches
|
|
2
|
-
|
|
3
|
-
fields:
|
|
4
|
-
- field: id
|
|
5
|
-
special:
|
|
6
|
-
- uuid
|
|
7
|
-
readonly: true
|
|
8
|
-
hidden: true
|
|
9
|
-
- field: name
|
|
10
|
-
- field: collection
|
|
11
|
-
- field: item
|
|
12
|
-
- field: hash
|
|
13
|
-
- field: date_created
|
|
14
|
-
special:
|
|
15
|
-
- date-created
|
|
16
|
-
- cast-timestamp
|
|
17
|
-
- field: user_created
|
|
18
|
-
special:
|
|
19
|
-
- user-created
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Item, PrimaryKey, Query } from '@directus/types';
|
|
2
|
-
import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
3
|
-
import { AuthorizationService } from './authorization.js';
|
|
4
|
-
import { CollectionsService } from './collections.js';
|
|
5
|
-
import { ItemsService } from './items.js';
|
|
6
|
-
export declare class BranchesService extends ItemsService {
|
|
7
|
-
authorizationService: AuthorizationService;
|
|
8
|
-
collectionsService: CollectionsService;
|
|
9
|
-
constructor(options: AbstractServiceOptions);
|
|
10
|
-
private validateCreateData;
|
|
11
|
-
private validateUpdateData;
|
|
12
|
-
getMainBranchItem(collection: string, item: PrimaryKey, query?: Query): Promise<Item>;
|
|
13
|
-
verifyHash(collection: string, item: PrimaryKey, hash: string): Promise<{
|
|
14
|
-
outdated: boolean;
|
|
15
|
-
mainHash: string;
|
|
16
|
-
}>;
|
|
17
|
-
getBranchCommits(key: PrimaryKey): Promise<Partial<Item>[]>;
|
|
18
|
-
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
19
|
-
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
20
|
-
updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
21
|
-
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
22
|
-
updateByQuery(query: Query, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
23
|
-
commit(key: PrimaryKey, data: Partial<Item>): Promise<Partial<Item>>;
|
|
24
|
-
merge(branch: PrimaryKey, mainHash: string, fields?: string[]): Promise<import("../types/items.js").PrimaryKey>;
|
|
25
|
-
}
|