Fix updates / relational inserts

This commit is contained in:
rijkvanzanten
2020-07-29 11:04:43 -04:00
parent 9406c582e9
commit 45556e5a61
7 changed files with 162 additions and 181 deletions

View File

@@ -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();
});

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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];
}
}

View File

@@ -15,7 +15,7 @@ declare global {
token: string | null;
collection: string;
sanitizedQuery: Query;
single?: boolean;
singleton?: boolean;
permissions?: Permission;
}
}