1
import { Action } from '@directus/constants';
2
import { useEnv } from '@directus/env';
3
import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
4
import { isSystemCollection } from '@directus/system-data';
12
} from '@directus/types';
13
import type Keyv from 'keyv';
14
import type { Knex } from 'knex';
15
import { assign, clone, cloneDeep, omit, pick, without } from 'lodash-es';
16
import { getCache } from '../cache.js';
17
import { translateDatabaseError } from '../database/errors/translate.js';
18
import { getHelpers } from '../database/helpers/index.js';
19
import getDatabase from '../database/index.js';
20
import runAST from '../database/run-ast.js';
21
import emitter from '../emitter.js';
22
import type { AbstractService, AbstractServiceOptions, ActionEventParams, MutationOptions } from '../types/index.js';
23
import getASTFromQuery from '../utils/get-ast-from-query.js';
24
import { shouldClearCache } from '../utils/should-clear-cache.js';
25
import { transaction } from '../utils/transaction.js';
26
import { validateKeys } from '../utils/validate-keys.js';
27
import { AuthorizationService } from './authorization.js';
28
import { PayloadService } from './payload.js';
32
export type QueryOptions = {
33
stripNonRequested?: boolean;
34
permissionsAction?: PermissionsAction;
38
export type MutationTracker = {
39
trackMutations: (count: number) => void;
40
getCount: () => number;
43
export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractService {
46
accountability: Accountability | null;
48
schema: SchemaOverview;
49
cache: Keyv<any> | null;
51
constructor(collection: string, options: AbstractServiceOptions) {
52
this.collection = collection;
53
this.knex = options.knex || getDatabase();
54
this.accountability = options.accountability || null;
55
this.eventScope = isSystemCollection(this.collection) ? this.collection.substring(9) : 'items';
56
this.schema = options.schema;
57
this.cache = getCache().cache;
63
* Create a fork of the current service, allowing instantiation with different options.
65
private fork(options?: Partial<AbstractServiceOptions>): ItemsService<AnyItem> {
66
const Service = this.constructor;
68
// ItemsService expects `collection` and `options` as parameters,
69
// while the other services only expect `options`
70
const isItemsService = Service.length === 2;
72
const newOptions = { knex: this.knex, accountability: this.accountability, schema: this.schema, ...options };
75
return new ItemsService(this.collection, newOptions);
78
return new (Service as new (options: AbstractServiceOptions) => this)(newOptions);
81
createMutationTracker(initialCount = 0): MutationTracker {
82
const maxCount = Number(env['MAX_BATCH_MUTATION']);
83
let mutationCount = initialCount;
85
trackMutations(count: number) {
86
mutationCount += count;
88
if (mutationCount > maxCount) {
89
throw new InvalidPayloadError({ reason: `Exceeded max batch mutation limit of ${maxCount}` });
98
async getKeysByQuery(query: Query): Promise<PrimaryKey[]> {
99
const primaryKeyField = this.schema.collections[this.collection]!.primary;
100
const readQuery = cloneDeep(query);
101
readQuery.fields = [primaryKeyField];
103
// Allow unauthenticated access
104
const itemsService = new ItemsService(this.collection, {
109
// We read the IDs of the items based on the query, and then run `updateMany`. `updateMany` does it's own
110
// permissions check for the keys, so we don't have to make this an authenticated read
111
const items = await itemsService.readByQuery(readQuery);
112
return items.map((item: AnyItem) => item[primaryKeyField]).filter((pk) => pk);
116
* Create a single new item.
118
async createOne(data: Partial<Item>, opts: MutationOptions = {}): Promise<PrimaryKey> {
119
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
121
if (!opts.bypassLimits) {
122
opts.mutationTracker.trackMutations(1);
125
const { ActivityService } = await import('./activity.js');
126
const { RevisionsService } = await import('./revisions.js');
128
const primaryKeyField = this.schema.collections[this.collection]!.primary;
129
const fields = Object.keys(this.schema.collections[this.collection]!.fields);
131
const aliases = Object.values(this.schema.collections[this.collection]!.fields)
132
.filter((field) => field.alias === true)
133
.map((field) => field.field);
135
const payload: AnyItem = cloneDeep(data);
136
const nestedActionEvents: ActionEventParams[] = [];
138
// By wrapping the logic in a transaction, we make sure we automatically roll back all the
139
// changes in the DB if any of the parts contained within throws an error. This also means
140
// that any errors thrown in any nested relational changes will bubble up and cancel the whole
142
const primaryKey: PrimaryKey = await transaction(this.knex, async (trx) => {
143
// We're creating new services instances so they can use the transaction as their Knex interface
144
const payloadService = new PayloadService(this.collection, {
145
accountability: this.accountability,
150
const authorizationService = new AuthorizationService({
151
accountability: this.accountability,
156
// Run all hooks that are attached to this event so the end user has the chance to augment the
157
// item that is about to be saved
158
const payloadAfterHooks =
159
opts.emitEvents !== false
160
? await emitter.emitFilter(
161
this.eventScope === 'items'
162
? ['items.create', `${this.collection}.items.create`]
163
: `${this.eventScope}.create`,
166
collection: this.collection,
171
accountability: this.accountability,
176
const payloadWithPresets = this.accountability
177
? authorizationService.validatePayload('create', this.collection, payloadAfterHooks)
180
if (opts.preMutationError) {
181
throw opts.preMutationError;
185
payload: payloadWithM2O,
186
revisions: revisionsM2O,
187
nestedActionEvents: nestedActionEventsM2O,
188
} = await payloadService.processM2O(payloadWithPresets, opts);
191
payload: payloadWithA2O,
192
revisions: revisionsA2O,
193
nestedActionEvents: nestedActionEventsA2O,
194
} = await payloadService.processA2O(payloadWithM2O, opts);
196
const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
197
const payloadWithTypeCasting = await payloadService.processValues('create', payloadWithoutAliases);
199
// The primary key can already exist in the payload.
200
// In case of manual string / UUID primary keys it's always provided at this point.
201
// In case of an (big) integer primary key, it might be provided as the user can specify the value manually.
202
let primaryKey: undefined | PrimaryKey = payloadWithTypeCasting[primaryKeyField];
205
validateKeys(this.schema, this.collection, primaryKeyField, primaryKey);
208
// If a PK of type number was provided, although the PK is set the auto_increment,
209
// depending on the database, the sequence might need to be reset to protect future PK collisions.
210
let autoIncrementSequenceNeedsToBeReset = false;
212
const pkField = this.schema.collections[this.collection]!.fields[primaryKeyField];
217
!opts.bypassAutoIncrementSequenceReset &&
218
['integer', 'bigInteger'].includes(pkField.type) &&
219
pkField.defaultValue === 'AUTO_INCREMENT'
221
autoIncrementSequenceNeedsToBeReset = true;
225
const result = await trx
226
.insert(payloadWithoutAliases)
227
.into(this.collection)
228
.returning(primaryKeyField)
229
.then((result) => result[0]);
231
const returnedKey = typeof result === 'object' ? result[primaryKeyField] : result;
233
if (pkField!.type === 'uuid') {
234
primaryKey = getHelpers(trx).schema.formatUUID(primaryKey ?? returnedKey);
236
primaryKey = primaryKey ?? returnedKey;
239
const dbError = await translateDatabaseError(err);
241
if (isDirectusError(dbError, ErrorCode.RecordNotUnique) && dbError.extensions.primaryKey) {
242
// This is a MySQL specific thing we need to handle here, since MySQL does not return the field name
243
// if the unique constraint is the primary key
244
dbError.extensions.field = pkField?.field ?? null;
246
delete dbError.extensions.primaryKey;
252
// Most database support returning, those who don't tend to return the PK anyways
253
// (MySQL/SQLite). In case the primary key isn't know yet, we'll do a best-attempt at
254
// fetching it based on the last inserted row
256
// Fetching it with max should be safe, as we're in the context of the current transaction
257
const result = await trx.max(primaryKeyField, { as: 'id' }).from(this.collection).first();
258
primaryKey = result.id;
259
// Set the primary key on the input item, in order for the "after" event hook to be able
261
payload[primaryKeyField] = primaryKey;
264
// At this point, the primary key is guaranteed to be set.
265
primaryKey = primaryKey as PrimaryKey;
267
const { revisions: revisionsO2M, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(
273
nestedActionEvents.push(...nestedActionEventsM2O);
274
nestedActionEvents.push(...nestedActionEventsA2O);
275
nestedActionEvents.push(...nestedActionEventsO2M);
277
// If this is an authenticated action, and accountability tracking is enabled, save activity row
278
if (this.accountability && this.schema.collections[this.collection]!.accountability !== null) {
279
const activityService = new ActivityService({
284
const activity = await activityService.createOne({
285
action: Action.CREATE,
286
user: this.accountability!.user,
287
collection: this.collection,
288
ip: this.accountability!.ip,
289
user_agent: this.accountability!.userAgent,
290
origin: this.accountability!.origin,
294
// If revisions are tracked, create revisions record
295
if (this.schema.collections[this.collection]!.accountability === 'all') {
296
const revisionsService = new RevisionsService({
301
const revisionDelta = await payloadService.prepareDelta(payloadAfterHooks);
303
const revision = await revisionsService.createOne({
305
collection: this.collection,
308
delta: revisionDelta,
311
// Make sure to set the parent field of the child-revision rows
312
const childrenRevisions = [...revisionsM2O, ...revisionsA2O, ...revisionsO2M];
314
if (childrenRevisions.length > 0) {
315
await revisionsService.updateMany(childrenRevisions, { parent: revision });
318
if (opts.onRevisionCreate) {
319
opts.onRevisionCreate(revision);
324
if (autoIncrementSequenceNeedsToBeReset) {
325
await getHelpers(trx).sequence.resetAutoIncrementSequence(this.collection, primaryKeyField);
331
if (opts.emitEvents !== false) {
332
const actionEvent = {
334
this.eventScope === 'items'
335
? ['items.create', `${this.collection}.items.create`]
336
: `${this.eventScope}.create`,
340
collection: this.collection,
343
database: getDatabase(),
345
accountability: this.accountability,
349
if (opts.bypassEmitAction) {
350
opts.bypassEmitAction(actionEvent);
352
emitter.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
355
for (const nestedActionEvent of nestedActionEvents) {
356
if (opts.bypassEmitAction) {
357
opts.bypassEmitAction(nestedActionEvent);
359
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
364
if (shouldClearCache(this.cache, opts, this.collection)) {
365
await this.cache.clear();
372
* Create multiple new items at once. Inserts all provided records sequentially wrapped in a transaction.
374
* Uses `this.createOne` under the hood.
376
async createMany(data: Partial<Item>[], opts: MutationOptions = {}): Promise<PrimaryKey[]> {
377
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
379
const { primaryKeys, nestedActionEvents } = await transaction(this.knex, async (knex) => {
380
const service = this.fork({ knex });
382
const primaryKeys: PrimaryKey[] = [];
383
const nestedActionEvents: ActionEventParams[] = [];
385
const pkField = this.schema.collections[this.collection]!.primary;
387
for (const [index, payload] of data.entries()) {
388
let bypassAutoIncrementSequenceReset = true;
390
// the auto_increment sequence needs to be reset if the current item contains a manual PK and
391
// 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
392
if (payload[pkField] && (index === data.length - 1 || !data[index + 1]?.[pkField])) {
393
bypassAutoIncrementSequenceReset = false;
396
const primaryKey = await service.createOne(payload, {
398
autoPurgeCache: false,
399
bypassEmitAction: (params) => nestedActionEvents.push(params),
400
mutationTracker: opts.mutationTracker,
401
bypassAutoIncrementSequenceReset,
404
primaryKeys.push(primaryKey);
407
return { primaryKeys, nestedActionEvents };
410
if (opts.emitEvents !== false) {
411
for (const nestedActionEvent of nestedActionEvents) {
412
if (opts.bypassEmitAction) {
413
opts.bypassEmitAction(nestedActionEvent);
415
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
420
if (shouldClearCache(this.cache, opts, this.collection)) {
421
await this.cache.clear();
428
* Get items by query.
430
async readByQuery(query: Query, opts?: QueryOptions): Promise<Item[]> {
432
opts?.emitEvents !== false
433
? await emitter.emitFilter(
434
this.eventScope === 'items'
435
? ['items.query', `${this.collection}.items.query`]
436
: `${this.eventScope}.query`,
439
collection: this.collection,
444
accountability: this.accountability,
449
let ast = await getASTFromQuery(this.collection, updatedQuery, this.schema, {
450
accountability: this.accountability,
451
// By setting the permissions action, you can read items using the permissions for another
452
// operation's permissions. This is used to dynamically check if you have update/delete
453
// access to (a) certain item(s)
454
action: opts?.permissionsAction || 'read',
458
if (this.accountability && this.accountability.admin !== true) {
459
const authorizationService = new AuthorizationService({
460
accountability: this.accountability,
465
ast = await authorizationService.processAST(ast, opts?.permissionsAction);
468
const records = await runAST(ast, this.schema, {
470
// GraphQL requires relational keys to be returned regardless
471
stripNonRequested: opts?.stripNonRequested !== undefined ? opts.stripNonRequested : true,
474
if (records === null) {
475
throw new ForbiddenError();
478
const filteredRecords =
479
opts?.emitEvents !== false
480
? await emitter.emitFilter(
481
this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`,
485
collection: this.collection,
490
accountability: this.accountability,
495
if (opts?.emitEvents !== false) {
497
this.eventScope === 'items' ? ['items.read', `${this.collection}.items.read`] : `${this.eventScope}.read`,
499
payload: filteredRecords,
501
collection: this.collection,
504
database: this.knex || getDatabase(),
506
accountability: this.accountability,
511
return filteredRecords as Item[];
515
* Get single item by primary key.
517
* Uses `this.readByQuery` under the hood.
519
async readOne(key: PrimaryKey, query: Query = {}, opts?: QueryOptions): Promise<Item> {
520
const primaryKeyField = this.schema.collections[this.collection]!.primary;
521
validateKeys(this.schema, this.collection, primaryKeyField, key);
523
const filterWithKey = assign({}, query.filter, { [primaryKeyField]: { _eq: key } });
524
const queryWithKey = assign({}, query, { filter: filterWithKey });
526
const results = await this.readByQuery(queryWithKey, opts);
528
if (results.length === 0) {
529
throw new ForbiddenError();
536
* Get multiple items by primary keys.
538
* Uses `this.readByQuery` under the hood.
540
async readMany(keys: PrimaryKey[], query: Query = {}, opts?: QueryOptions): Promise<Item[]> {
541
const primaryKeyField = this.schema.collections[this.collection]!.primary;
542
validateKeys(this.schema, this.collection, primaryKeyField, keys);
544
const filterWithKey = { _and: [{ [primaryKeyField]: { _in: keys } }, query.filter ?? {}] };
545
const queryWithKey = assign({}, query, { filter: filterWithKey });
547
// Set query limit as the number of keys
548
if (Array.isArray(keys) && keys.length > 0 && !queryWithKey.limit) {
549
queryWithKey.limit = keys.length;
552
const results = await this.readByQuery(queryWithKey, opts);
558
* Update multiple items by query.
560
* Uses `this.updateMany` under the hood.
562
async updateByQuery(query: Query, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]> {
563
const keys = await this.getKeysByQuery(query);
565
const primaryKeyField = this.schema.collections[this.collection]!.primary;
566
validateKeys(this.schema, this.collection, primaryKeyField, keys);
568
return keys.length ? await this.updateMany(keys, data, opts) : [];
572
* Update a single item by primary key.
574
* Uses `this.updateMany` under the hood.
576
async updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
577
const primaryKeyField = this.schema.collections[this.collection]!.primary;
578
validateKeys(this.schema, this.collection, primaryKeyField, key);
580
await this.updateMany([key], data, opts);
585
* Update multiple items in a single transaction.
587
* Uses `this.updateOne` under the hood.
589
async updateBatch(data: Partial<Item>[], opts: MutationOptions = {}): Promise<PrimaryKey[]> {
590
if (!Array.isArray(data)) {
591
throw new InvalidPayloadError({ reason: 'Input should be an array of items' });
594
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
596
const primaryKeyField = this.schema.collections[this.collection]!.primary;
598
const keys: PrimaryKey[] = [];
601
await transaction(this.knex, async (knex) => {
602
const service = this.fork({ knex });
604
for (const item of data) {
605
const primaryKey = item[primaryKeyField];
606
if (!primaryKey) throw new InvalidPayloadError({ reason: `Item in update misses primary key` });
608
const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
609
keys.push(await service.updateOne(primaryKey, omit(item, primaryKeyField), combinedOpts));
613
if (shouldClearCache(this.cache, opts, this.collection)) {
614
await this.cache.clear();
622
* Update many items by primary key, setting all items to the same change.
624
async updateMany(keys: PrimaryKey[], data: Partial<Item>, opts: MutationOptions = {}): Promise<PrimaryKey[]> {
625
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
627
if (!opts.bypassLimits) {
628
opts.mutationTracker.trackMutations(keys.length);
631
const { ActivityService } = await import('./activity.js');
632
const { RevisionsService } = await import('./revisions.js');
634
const primaryKeyField = this.schema.collections[this.collection]!.primary;
635
validateKeys(this.schema, this.collection, primaryKeyField, keys);
637
const fields = Object.keys(this.schema.collections[this.collection]!.fields);
639
const aliases = Object.values(this.schema.collections[this.collection]!.fields)
640
.filter((field) => field.alias === true)
641
.map((field) => field.field);
643
const payload: Partial<AnyItem> = cloneDeep(data);
644
const nestedActionEvents: ActionEventParams[] = [];
646
const authorizationService = new AuthorizationService({
647
accountability: this.accountability,
652
// Run all hooks that are attached to this event so the end user has the chance to augment the
653
// item that is about to be saved
654
const payloadAfterHooks =
655
opts.emitEvents !== false
656
? await emitter.emitFilter(
657
this.eventScope === 'items'
658
? ['items.update', `${this.collection}.items.update`]
659
: `${this.eventScope}.update`,
663
collection: this.collection,
668
accountability: this.accountability,
673
// Sort keys to ensure that the order is maintained
676
if (this.accountability) {
677
await authorizationService.checkAccess('update', this.collection, keys);
680
const payloadWithPresets = this.accountability
681
? authorizationService.validatePayload('update', this.collection, payloadAfterHooks)
684
if (opts.preMutationError) {
685
throw opts.preMutationError;
688
await transaction(this.knex, async (trx) => {
689
const payloadService = new PayloadService(this.collection, {
690
accountability: this.accountability,
696
payload: payloadWithM2O,
697
revisions: revisionsM2O,
698
nestedActionEvents: nestedActionEventsM2O,
699
} = await payloadService.processM2O(payloadWithPresets, opts);
702
payload: payloadWithA2O,
703
revisions: revisionsA2O,
704
nestedActionEvents: nestedActionEventsA2O,
705
} = await payloadService.processA2O(payloadWithM2O, opts);
707
const payloadWithoutAliasAndPK = pick(payloadWithA2O, without(fields, primaryKeyField, ...aliases));
708
const payloadWithTypeCasting = await payloadService.processValues('update', payloadWithoutAliasAndPK);
710
if (Object.keys(payloadWithTypeCasting).length > 0) {
712
await trx(this.collection).update(payloadWithTypeCasting).whereIn(primaryKeyField, keys);
714
throw await translateDatabaseError(err);
718
const childrenRevisions = [...revisionsM2O, ...revisionsA2O];
720
nestedActionEvents.push(...nestedActionEventsM2O);
721
nestedActionEvents.push(...nestedActionEventsA2O);
723
for (const key of keys) {
724
const { revisions, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(
730
childrenRevisions.push(...revisions);
731
nestedActionEvents.push(...nestedActionEventsO2M);
734
// If this is an authenticated action, and accountability tracking is enabled, save activity row
735
if (this.accountability && this.schema.collections[this.collection]!.accountability !== null) {
736
const activityService = new ActivityService({
741
const activity = await activityService.createMany(
743
action: Action.UPDATE,
744
user: this.accountability!.user,
745
collection: this.collection,
746
ip: this.accountability!.ip,
747
user_agent: this.accountability!.userAgent,
748
origin: this.accountability!.origin,
751
{ bypassLimits: true },
754
if (this.schema.collections[this.collection]!.accountability === 'all') {
755
const itemsService = new ItemsService(this.collection, {
760
const snapshots = await itemsService.readMany(keys);
762
const revisionsService = new RevisionsService({
769
activity.map(async (activity, index) => ({
771
collection: this.collection,
774
snapshots && Array.isArray(snapshots) ? JSON.stringify(snapshots[index]) : JSON.stringify(snapshots),
775
delta: await payloadService.prepareDelta(payloadWithTypeCasting),
778
).filter((revision) => revision.delta);
780
const revisionIDs = await revisionsService.createMany(revisions);
782
for (let i = 0; i < revisionIDs.length; i++) {
783
const revisionID = revisionIDs[i]!;
785
if (opts.onRevisionCreate) {
786
opts.onRevisionCreate(revisionID);
790
// In case of a nested relational creation/update in a updateMany, the nested m2o/a2o
791
// creation is only done once. We treat the first updated item as the "main" update,
792
// with all other revisions on the current level as regular "flat" updates, and
793
// nested revisions as children of this first "root" item.
794
if (childrenRevisions.length > 0) {
795
await revisionsService.updateMany(childrenRevisions, { parent: revisionID });
803
if (shouldClearCache(this.cache, opts, this.collection)) {
804
await this.cache.clear();
807
if (opts.emitEvents !== false) {
808
const actionEvent = {
810
this.eventScope === 'items'
811
? ['items.update', `${this.collection}.items.update`]
812
: `${this.eventScope}.update`,
814
payload: payloadWithPresets,
816
collection: this.collection,
819
database: getDatabase(),
821
accountability: this.accountability,
825
if (opts.bypassEmitAction) {
826
opts.bypassEmitAction(actionEvent);
828
emitter.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
831
for (const nestedActionEvent of nestedActionEvents) {
832
if (opts.bypassEmitAction) {
833
opts.bypassEmitAction(nestedActionEvent);
835
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
844
* Upsert a single item.
846
* Uses `this.createOne` / `this.updateOne` under the hood.
848
async upsertOne(payload: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
849
const primaryKeyField = this.schema.collections[this.collection]!.primary;
850
const primaryKey: PrimaryKey | undefined = payload[primaryKeyField];
853
validateKeys(this.schema, this.collection, primaryKeyField, primaryKey);
859
.select(primaryKeyField)
860
.from(this.collection)
861
.where({ [primaryKeyField]: primaryKey })
865
return await this.updateOne(primaryKey as PrimaryKey, payload, opts);
867
return await this.createOne(payload, opts);
874
* Uses `this.upsertOne` under the hood.
876
async upsertMany(payloads: Partial<Item>[], opts: MutationOptions = {}): Promise<PrimaryKey[]> {
877
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
879
const primaryKeys = await transaction(this.knex, async (knex) => {
880
const service = this.fork({ knex });
882
const primaryKeys: PrimaryKey[] = [];
884
for (const payload of payloads) {
885
const primaryKey = await service.upsertOne(payload, { ...(opts || {}), autoPurgeCache: false });
886
primaryKeys.push(primaryKey);
892
if (shouldClearCache(this.cache, opts, this.collection)) {
893
await this.cache.clear();
900
* Delete multiple items by query.
902
* Uses `this.deleteMany` under the hood.
904
async deleteByQuery(query: Query, opts?: MutationOptions): Promise<PrimaryKey[]> {
905
const keys = await this.getKeysByQuery(query);
907
const primaryKeyField = this.schema.collections[this.collection]!.primary;
908
validateKeys(this.schema, this.collection, primaryKeyField, keys);
910
return keys.length ? await this.deleteMany(keys, opts) : [];
914
* Delete a single item by primary key.
916
* Uses `this.deleteMany` under the hood.
918
async deleteOne(key: PrimaryKey, opts?: MutationOptions): Promise<PrimaryKey> {
919
const primaryKeyField = this.schema.collections[this.collection]!.primary;
920
validateKeys(this.schema, this.collection, primaryKeyField, key);
922
await this.deleteMany([key], opts);
927
* Delete multiple items by primary key.
929
async deleteMany(keys: PrimaryKey[], opts: MutationOptions = {}): Promise<PrimaryKey[]> {
930
if (!opts.mutationTracker) opts.mutationTracker = this.createMutationTracker();
932
if (!opts.bypassLimits) {
933
opts.mutationTracker.trackMutations(keys.length);
936
const { ActivityService } = await import('./activity.js');
938
const primaryKeyField = this.schema.collections[this.collection]!.primary;
939
validateKeys(this.schema, this.collection, primaryKeyField, keys);
941
if (this.accountability && this.accountability.admin !== true) {
942
const authorizationService = new AuthorizationService({
943
accountability: this.accountability,
948
await authorizationService.checkAccess('delete', this.collection, keys);
951
if (opts.preMutationError) {
952
throw opts.preMutationError;
955
if (opts.emitEvents !== false) {
956
await emitter.emitFilter(
957
this.eventScope === 'items' ? ['items.delete', `${this.collection}.items.delete`] : `${this.eventScope}.delete`,
960
collection: this.collection,
965
accountability: this.accountability,
970
await transaction(this.knex, async (trx) => {
971
await trx(this.collection).whereIn(primaryKeyField, keys).delete();
973
if (this.accountability && this.schema.collections[this.collection]!.accountability !== null) {
974
const activityService = new ActivityService({
979
await activityService.createMany(
981
action: Action.DELETE,
982
user: this.accountability!.user,
983
collection: this.collection,
984
ip: this.accountability!.ip,
985
user_agent: this.accountability!.userAgent,
986
origin: this.accountability!.origin,
989
{ bypassLimits: true },
994
if (shouldClearCache(this.cache, opts, this.collection)) {
995
await this.cache.clear();
998
if (opts.emitEvents !== false) {
999
const actionEvent = {
1001
this.eventScope === 'items'
1002
? ['items.delete', `${this.collection}.items.delete`]
1003
: `${this.eventScope}.delete`,
1007
collection: this.collection,
1010
database: getDatabase(),
1011
schema: this.schema,
1012
accountability: this.accountability,
1016
if (opts.bypassEmitAction) {
1017
opts.bypassEmitAction(actionEvent);
1019
emitter.emitAction(actionEvent.event, actionEvent.meta, actionEvent.context);
1027
* Read/treat collection as singleton.
1029
async readSingleton(query: Query, opts?: QueryOptions): Promise<Partial<Item>> {
1030
query = clone(query);
1034
const records = await this.readByQuery(query, opts);
1035
const record = records[0];
1038
let fields = Object.entries(this.schema.collections[this.collection]!.fields);
1039
const defaults: Record<string, any> = {};
1041
if (query.fields && query.fields.includes('*') === false) {
1042
fields = fields.filter(([name]) => {
1043
return query.fields!.includes(name);
1047
for (const [name, field] of fields) {
1048
if (this.schema.collections[this.collection]!.primary === name) {
1049
defaults[name] = null;
1053
if (field.defaultValue !== null) defaults[name] = field.defaultValue;
1056
return defaults as Partial<Item>;
1063
* Upsert/treat collection as singleton.
1065
* Uses `this.createOne` / `this.updateOne` under the hood.
1067
async upsertSingleton(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey> {
1068
const primaryKeyField = this.schema.collections[this.collection]!.primary;
1070
const record = await this.knex.select(primaryKeyField).from(this.collection).limit(1).first();
1073
return await this.updateOne(record[primaryKeyField], data, opts);
1076
return await this.createOne(data, opts);