mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Fix updates / relational inserts
This commit is contained in:
@@ -19,12 +19,12 @@ const collectionExists: RequestHandler = asyncHandler(async (req, res, next) =>
|
||||
req.collection = req.params.collection;
|
||||
|
||||
const collectionInfo = await database
|
||||
.select('single')
|
||||
.select('singleton')
|
||||
.from('directus_collections')
|
||||
.where({ collection: req.collection })
|
||||
.first();
|
||||
|
||||
req.single = collectionInfo?.single || false;
|
||||
req.singleton = collectionInfo?.singleton || false;
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ router.post(
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single) {
|
||||
if (req.singleton) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ router.get(
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
|
||||
const [records, meta] = await Promise.all([
|
||||
req.single
|
||||
req.singleton
|
||||
? service.readSingleton(req.sanitizedQuery)
|
||||
: service.readByQuery(req.sanitizedQuery),
|
||||
MetaService.getMetaForQuery(req.collection, req.sanitizedQuery),
|
||||
@@ -52,7 +52,7 @@ router.get(
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single) {
|
||||
if (req.singleton) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ router.patch(
|
||||
asyncHandler(async (req, res) => {
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
|
||||
if (req.single === true) {
|
||||
if (req.singleton === true) {
|
||||
await service.upsertSingleton(req.body);
|
||||
const item = await service.readSingleton(req.sanitizedQuery);
|
||||
|
||||
@@ -89,7 +89,7 @@ router.patch(
|
||||
collectionExists,
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
if (req.single) {
|
||||
if (req.singleton) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
|
||||
@@ -162,6 +162,9 @@ export default class AuthorizationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided payload matches the configured permissions, and adds the presets to the payload.
|
||||
*/
|
||||
processValues(
|
||||
operation: Operation,
|
||||
collection: string,
|
||||
|
||||
@@ -47,7 +47,7 @@ export const create = async (payload: any, accountability?: Accountability) => {
|
||||
const primaryKey = await itemsService.create({
|
||||
collection: payload.collection,
|
||||
hidden: payload.hidden || false,
|
||||
single: payload.single || false,
|
||||
singleton: payload.singleton || false,
|
||||
icon: payload.icon || null,
|
||||
note: payload.note || null,
|
||||
translation: payload.translation || null,
|
||||
@@ -88,7 +88,7 @@ export const readAll = async (query: Query, accountability?: Accountability) =>
|
||||
collection: table.name,
|
||||
note: table.comment,
|
||||
hidden: collectionInfo?.hidden || false,
|
||||
single: collectionInfo?.single || false,
|
||||
singleton: collectionInfo?.singleton || false,
|
||||
icon: collectionInfo?.icon || null,
|
||||
translation: collectionInfo?.translation || null,
|
||||
};
|
||||
@@ -113,7 +113,7 @@ export const readOne = async (
|
||||
collection: table.name,
|
||||
note: table.comment,
|
||||
hidden: collectionInfo[0]?.hidden || false,
|
||||
single: collectionInfo[0]?.single || false,
|
||||
singleton: collectionInfo[0]?.singleton || false,
|
||||
icon: collectionInfo[0]?.icon || null,
|
||||
translation: collectionInfo[0]?.translation || null,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import database from '../database';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import runAST from '../database/run-ast';
|
||||
import getASTFromQuery from '../utils/get-ast-from-query';
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ import AuthorizationService from './authorization';
|
||||
|
||||
import { pick, clone } from 'lodash';
|
||||
import getDefaultValue from '../utils/get-default-value';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
|
||||
export default class ItemsService implements AbstractService {
|
||||
collection: string;
|
||||
@@ -35,25 +37,36 @@ export default class ItemsService implements AbstractService {
|
||||
async create(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
||||
async create(data: Partial<Item>): Promise<PrimaryKey>;
|
||||
async create(data: Partial<Item> | Partial<Item>[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
const primaryKeyField = await schemaInspector.primary(this.collection);
|
||||
const columns = await schemaInspector.columns(this.collection);
|
||||
|
||||
let payloads = clone(Array.isArray(data) ? data : [data]);
|
||||
|
||||
const savedPrimaryKeys = await this.knex.transaction(async (trx) => {
|
||||
const payloadService = new PayloadService(this.collection, { knex: trx });
|
||||
const authorizationService = new AuthorizationService({ knex: trx });
|
||||
const payloadService = new PayloadService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
});
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
});
|
||||
|
||||
payloads = await payloadService.processValues('create', payloads);
|
||||
payloads = await payloadService.processM2O(payloads);
|
||||
|
||||
const payloadsWithoutAliases = payloads.map((payload) =>
|
||||
let payloadsWithoutAliases = payloads.map((payload) =>
|
||||
pick(
|
||||
payload,
|
||||
columns.map(({ column }) => column)
|
||||
)
|
||||
);
|
||||
|
||||
payloadsWithoutAliases = await payloadService.processValues(
|
||||
'create',
|
||||
payloadsWithoutAliases
|
||||
);
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
payloads = await authorizationService.processValues(
|
||||
'create',
|
||||
@@ -113,6 +126,7 @@ export default class ItemsService implements AbstractService {
|
||||
query: Query = {},
|
||||
operation: Operation = 'read'
|
||||
): Promise<Item | Item[]> {
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
const payloadService = new PayloadService(this.collection);
|
||||
const primaryKeyField = await schemaInspector.primary(this.collection);
|
||||
const keys = Array.isArray(key) ? key : [key];
|
||||
@@ -154,89 +168,78 @@ export default class ItemsService implements AbstractService {
|
||||
data: Partial<Item> | Partial<Item>[],
|
||||
key?: PrimaryKey | PrimaryKey[]
|
||||
): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
return 15; /** nothing to see here 👀 */
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
const primaryKeyField = await schemaInspector.primary(this.collection);
|
||||
const columns = await schemaInspector.columns(this.collection);
|
||||
|
||||
// // /**
|
||||
// // * Update one or more items to the given payload
|
||||
// // */
|
||||
// export async function updateItem<T extends PrimaryKey | PrimaryKey[]>(
|
||||
// collection: string,
|
||||
// primaryKey: T,
|
||||
// data: Partial<Item>,
|
||||
// accountability?: Accountability
|
||||
// ): Promise<T> {
|
||||
// const primaryKeys = (Array.isArray(primaryKey) ? primaryKey : [primaryKey]) as PrimaryKey[];
|
||||
// Updating one or more items to the same payload
|
||||
if (data && key) {
|
||||
const keys = Array.isArray(key) ? key : [key];
|
||||
|
||||
// let payload = clone(data);
|
||||
let payload = clone(data);
|
||||
|
||||
// if (accountability?.admin !== true) {
|
||||
// payload = await PermissionsService.processValues(
|
||||
// 'validate',
|
||||
// collection,
|
||||
// accountability?.role || null,
|
||||
// payload
|
||||
// );
|
||||
// }
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
});
|
||||
await authorizationService.checkAccess('update', this.collection, keys);
|
||||
payload = await authorizationService.processValues(
|
||||
'validate',
|
||||
this.collection,
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
// payload = await PayloadService.processValues('update', collection, payload);
|
||||
// payload = await PayloadService.processM2O(collection, payload);
|
||||
await this.knex.transaction(async (trx) => {
|
||||
const payloadService = new PayloadService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
});
|
||||
payload = await payloadService.processM2O(payload);
|
||||
payload = await payloadService.processValues('update', payload);
|
||||
const payloadWithoutAliases = pick(
|
||||
payload,
|
||||
columns.map(({ column }) => column)
|
||||
);
|
||||
await trx(this.collection)
|
||||
.update(payloadWithoutAliases)
|
||||
.whereIn(primaryKeyField, keys);
|
||||
await payloadService.processO2M(payload);
|
||||
|
||||
// const primaryKeyField = await schemaInspector.primary(collection);
|
||||
/**
|
||||
* @todo save activity
|
||||
*/
|
||||
});
|
||||
|
||||
// // Only insert the values that actually save to an existing column. This ensures we ignore aliases etc
|
||||
// const columns = await schemaInspector.columns(collection);
|
||||
return key;
|
||||
}
|
||||
|
||||
// const payloadWithoutAlias = pick(
|
||||
// payload,
|
||||
// columns.map(({ column }) => column)
|
||||
// );
|
||||
const keys: PrimaryKey[] = [];
|
||||
|
||||
// // Make sure the user has access to every item they're trying to update
|
||||
// await Promise.all(
|
||||
// primaryKeys.map(async (key) => {
|
||||
// if (accountability && accountability.admin === false) {
|
||||
// return await PermissionsService.checkAccess(
|
||||
// 'update',
|
||||
// collection,
|
||||
// key,
|
||||
// accountability.role
|
||||
// );
|
||||
// } else {
|
||||
// return Promise.resolve();
|
||||
// }
|
||||
// })
|
||||
// );
|
||||
await this.knex.transaction(async (trx) => {
|
||||
const itemsService = new ItemsService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
});
|
||||
|
||||
// // Save updates
|
||||
// await database.transaction(async (transaction) => {
|
||||
// for (const key of primaryKeys) {
|
||||
// await transaction(collection)
|
||||
// .where({ [primaryKeyField]: key })
|
||||
// .update(payloadWithoutAlias);
|
||||
for (const single of data as Partial<Item>[]) {
|
||||
let payload = clone(single);
|
||||
const key = payload[primaryKeyField];
|
||||
if (!key)
|
||||
throw new InvalidPayloadException('Primary key is missing in update payload.');
|
||||
keys.push(key);
|
||||
await itemsService.update(payload, key);
|
||||
}
|
||||
});
|
||||
|
||||
// if (accountability) {
|
||||
// await saveActivityAndRevision(
|
||||
// ActivityService.Action.UPDATE,
|
||||
// collection,
|
||||
// String(key),
|
||||
// payloadWithoutAlias,
|
||||
// accountability,
|
||||
// transaction
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// return;
|
||||
// });
|
||||
|
||||
// return primaryKey;
|
||||
// }
|
||||
return keys;
|
||||
}
|
||||
|
||||
delete(key: PrimaryKey): Promise<PrimaryKey>;
|
||||
delete(keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
async delete(key: PrimaryKey | PrimaryKey[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const keys = (Array.isArray(key) ? key : [key]) as PrimaryKey[];
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
const primaryKeyField = await schemaInspector.primary(this.collection);
|
||||
|
||||
if (this.accountability && this.accountability.admin !== false) {
|
||||
@@ -247,7 +250,7 @@ export default class ItemsService implements AbstractService {
|
||||
await authorizationService.checkAccess('delete', this.collection, key);
|
||||
}
|
||||
|
||||
await database.transaction(async (trx) => {
|
||||
await this.knex.transaction(async (trx) => {
|
||||
await trx(this.collection).whereIn(primaryKeyField, keys).delete();
|
||||
|
||||
if (this.accountability) {
|
||||
@@ -268,6 +271,7 @@ export default class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
async readSingleton(query: Query) {
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
query.limit = 1;
|
||||
|
||||
const records = await this.readByQuery(query);
|
||||
@@ -288,6 +292,7 @@ export default class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
async upsertSingleton(data: Partial<Item>) {
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
const primaryKeyField = await schemaInspector.primary(this.collection);
|
||||
|
||||
const record = await this.knex
|
||||
|
||||
@@ -8,7 +8,7 @@ import argon2 from 'argon2';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import database from '../database';
|
||||
import { clone, isObject } from 'lodash';
|
||||
import { Relation, Item, AbstractServiceOptions } from '../types';
|
||||
import { Relation, Item, AbstractServiceOptions, Accountability, PrimaryKey } from '../types';
|
||||
import ItemsService from './items';
|
||||
import { URL } from 'url';
|
||||
import Knex from 'knex';
|
||||
@@ -20,12 +20,14 @@ type Transformers = {
|
||||
};
|
||||
|
||||
export default class PayloadService {
|
||||
collection: string;
|
||||
accountability: Accountability | null;
|
||||
knex: Knex;
|
||||
collection: string;
|
||||
|
||||
constructor(collection: string, options?: AbstractServiceOptions) {
|
||||
this.collection = collection;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.knex = options?.knex || database;
|
||||
this.collection = collection;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -105,7 +107,8 @@ export default class PayloadService {
|
||||
processedPayload.map(async (record: any) => {
|
||||
await Promise.all(
|
||||
specialFieldsInCollection.map(async (field) => {
|
||||
record[field.field] = await this.processField(field, record, operation);
|
||||
const newValue = await this.processField(field, record, operation);
|
||||
if (newValue !== undefined) record[field.field] = newValue;
|
||||
})
|
||||
);
|
||||
})
|
||||
@@ -142,133 +145,103 @@ export default class PayloadService {
|
||||
return payload[field.field];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively save/update all nested related m2o items
|
||||
*/
|
||||
processM2O(payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
processM2O(payloads: Partial<Item>): Promise<Partial<Item>>;
|
||||
async processM2O(
|
||||
payload: Partial<Item> | Partial<Item>[]
|
||||
): Promise<Partial<Item> | Partial<Item>[]> {
|
||||
const itemsService = new ItemsService(this.collection, {
|
||||
knex: this.knex,
|
||||
});
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ many_collection: this.collection });
|
||||
|
||||
const payloads = Array.isArray(payload) ? payload : [payload];
|
||||
const payloads = clone(Array.isArray(payload) ? payload : [payload]);
|
||||
|
||||
const processedPayloads = [];
|
||||
|
||||
for (const payload of payloads) {
|
||||
const payloadClone = clone(payload);
|
||||
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ many_collection: this.collection });
|
||||
for (let i = 0; i < payloads.length; i++) {
|
||||
let payload = payloads[i];
|
||||
|
||||
// Only process related records that are actually in the payload
|
||||
const relationsToProcess = relations.filter((relation) => {
|
||||
return (
|
||||
payloadClone.hasOwnProperty(relation.many_field) &&
|
||||
isObject(payloadClone[relation.many_field])
|
||||
payload.hasOwnProperty(relation.many_field) &&
|
||||
isObject(payload[relation.many_field])
|
||||
);
|
||||
});
|
||||
|
||||
// Save all nested m2o records
|
||||
await Promise.all(
|
||||
relationsToProcess.map(async (relation) => {
|
||||
const relatedRecord: Partial<Item> = payloadClone[relation.many_field];
|
||||
const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.one_primary);
|
||||
for (const relation of relationsToProcess) {
|
||||
const itemsService = new ItemsService(relation.one_collection, {
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
});
|
||||
|
||||
let relatedPrimaryKey: string | number;
|
||||
const relatedRecord: Partial<Item> = payload[relation.many_field];
|
||||
const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.one_primary);
|
||||
|
||||
// if (hasPrimaryKey) {
|
||||
// relatedPrimaryKey = relatedRecord[relation.one_primary];
|
||||
// await itemsService.update(
|
||||
// relatedPrimaryKey,
|
||||
// relatedRecord
|
||||
// );
|
||||
// } else {
|
||||
// relatedPrimaryKey = await itemsService.create(
|
||||
// relation.one_collection,
|
||||
// relatedRecord
|
||||
// );
|
||||
// }
|
||||
let relatedPrimaryKey: PrimaryKey;
|
||||
|
||||
// Overwrite the nested object with just the primary key, so the parent level can be saved correctly
|
||||
// payloadClone[relation.many_field] = relatedPrimaryKey;
|
||||
})
|
||||
);
|
||||
if (hasPrimaryKey) {
|
||||
relatedPrimaryKey = relatedRecord[relation.one_primary];
|
||||
await itemsService.update(relatedRecord, relatedPrimaryKey);
|
||||
} else {
|
||||
relatedPrimaryKey = await itemsService.create(relatedRecord);
|
||||
}
|
||||
|
||||
processedPayloads.push(payloadClone);
|
||||
// Overwrite the nested object with just the primary key, so the parent level can be saved correctly
|
||||
payload[relation.many_field] = relatedPrimaryKey;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(payload) ? processedPayloads : processedPayloads[0];
|
||||
return Array.isArray(payload) ? payloads : payloads[0];
|
||||
}
|
||||
|
||||
processO2M(payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
processO2M(payloads: Partial<Item>): Promise<Partial<Item>>;
|
||||
async processO2M(
|
||||
payload: Partial<Item> | Partial<Item>[]
|
||||
): Promise<Partial<Item> | Partial<Item>[]> {
|
||||
const payloads = Array.isArray(payload) ? payload : [payload];
|
||||
/**
|
||||
* Recursively save/update all nested related o2m items
|
||||
*/
|
||||
async processO2M(payload: Partial<Item> | Partial<Item>[]) {
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ one_collection: this.collection });
|
||||
|
||||
const processedPayloads = [];
|
||||
const payloads = clone(Array.isArray(payload) ? payload : [payload]);
|
||||
|
||||
for (const payload of payloads) {
|
||||
const payloadClone = clone(payload);
|
||||
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ one_collection: this.collection });
|
||||
for (let i = 0; i < payloads.length; i++) {
|
||||
let payload = payloads[i];
|
||||
|
||||
// Only process related records that are actually in the payload
|
||||
const relationsToProcess = relations.filter((relation) => {
|
||||
return (
|
||||
payloadClone.hasOwnProperty(relation.one_field) &&
|
||||
Array.isArray(payloadClone[relation.one_field])
|
||||
payload.hasOwnProperty(relation.one_field) &&
|
||||
Array.isArray(payload[relation.one_field])
|
||||
);
|
||||
});
|
||||
|
||||
// Save all nested o2m records
|
||||
await Promise.all(
|
||||
relationsToProcess.map(async (relation) => {
|
||||
const relatedRecords = payloadClone[relation.one_field];
|
||||
for (const relation of relationsToProcess) {
|
||||
const relatedRecords: Partial<Item>[] = payload[relation.one_field].map(
|
||||
(record: Partial<Item>) => ({
|
||||
...record,
|
||||
[relation.many_field]: payload[relation.one_primary],
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
relatedRecords.map(async (relatedRecord: Partial<Item>, index: number) => {
|
||||
relatedRecord[relation.many_field] = payloadClone[relation.one_primary];
|
||||
const itemsService = new ItemsService(relation.many_collection, {
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
});
|
||||
|
||||
const hasPrimaryKey = relatedRecord.hasOwnProperty(
|
||||
relation.many_primary
|
||||
);
|
||||
const toBeCreated = relatedRecords.filter(
|
||||
(record) => record.hasOwnProperty(relation.many_primary) === false
|
||||
);
|
||||
const toBeUpdated = relatedRecords.filter(
|
||||
(record) => record.hasOwnProperty(relation.many_primary) === true
|
||||
);
|
||||
|
||||
let relatedPrimaryKey: string | number;
|
||||
|
||||
// if (hasPrimaryKey) {
|
||||
// relatedPrimaryKey = relatedRecord[relation.many_primary];
|
||||
|
||||
// await ItemsService.updateItem(
|
||||
// relation.many_collection,
|
||||
// relatedPrimaryKey,
|
||||
// relatedRecord
|
||||
// );
|
||||
// } else {
|
||||
// relatedPrimaryKey = await ItemsService.createItem(
|
||||
// relation.many_collection,
|
||||
// relatedRecord
|
||||
// );
|
||||
// }
|
||||
|
||||
// relatedRecord[relation.many_primary] = relatedPrimaryKey;
|
||||
|
||||
// payloadClone[relation.one_field][index] = relatedRecord;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
processedPayloads.push(payloadClone);
|
||||
await itemsService.create(toBeCreated);
|
||||
await itemsService.update(toBeUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(payload) ? processedPayloads : processedPayloads[0];
|
||||
}
|
||||
}
|
||||
|
||||
2
src/types/express.d.ts
vendored
2
src/types/express.d.ts
vendored
@@ -15,7 +15,7 @@ declare global {
|
||||
token: string | null;
|
||||
collection: string;
|
||||
sanitizedQuery: Query;
|
||||
single?: boolean;
|
||||
singleton?: boolean;
|
||||
permissions?: Permission;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user