Add batch type overrides + Item type

This commit is contained in:
rijkvanzanten
2020-07-17 14:42:12 -04:00
parent acd8ee1460
commit a24fa72373
18 changed files with 159 additions and 169 deletions

View File

@@ -6,6 +6,7 @@ import * as FilesService from '../services/files';
import logger from '../logger';
import { InvalidPayloadException } from '../exceptions';
import useCollection from '../middleware/use-collection';
import { Item } from '../types';
const router = express.Router();
@@ -14,7 +15,7 @@ router.use(useCollection('directus_files'));
const multipartHandler = (operation: 'create' | 'update') =>
asyncHandler(async (req, res, next) => {
const busboy = new Busboy({ headers: req.headers });
const savedFiles: Record<string, any> = [];
const savedFiles: Item[] = [];
/**
* The order of the fields in multipart/form-data is important. We require that all fields
@@ -23,7 +24,7 @@ const multipartHandler = (operation: 'create' | 'update') =>
*/
let disk: string;
let payload: Record<string, any> = {};
let payload: Partial<Item> = {};
busboy.on('field', (fieldname, val) => {
if (fieldname === 'storage') {
@@ -134,7 +135,7 @@ router.patch(
'/:pk',
sanitizeQuery,
asyncHandler(async (req, res, next) => {
let file: Record<string, any>;
let file: Item;
if (req.is('multipart/form-data')) {
file = await multipartHandler('update')(req, res, next);

View File

@@ -26,29 +26,16 @@ router.post(
userAgent: req.get('user-agent'),
};
const isBatch = Array.isArray(req.body);
const primaryKey = await ItemsService.createItem(req.collection, req.body, accountability);
if (isBatch) {
const body: Record<string, any>[] = req.body;
const primaryKeys = await ItemsService.createItem(req.collection, body, accountability);
const items = await ItemsService.readItem(
req.collection,
primaryKeys,
req.sanitizedQuery,
accountability
);
res.json({ data: items || null });
} else {
const body: Record<string, any> = req.body;
const primaryKey = await ItemsService.createItem(req.collection, body, accountability);
const item = await ItemsService.readItem(
req.collection,
primaryKey,
req.sanitizedQuery,
accountability
);
res.json({ data: item || null });
}
const result = await ItemsService.readItem(
req.collection,
primaryKey,
req.sanitizedQuery,
accountability
);
res.json({ data: result || null });
})
);
@@ -132,39 +119,30 @@ router.patch(
throw new RouteNotFoundException(req.path);
}
const primaryKey = req.params.pk;
const isBatch = primaryKey.includes(',');
const accountability: Accountability = {
user: req.user,
role: req.role,
admin: req.admin,
ip: req.ip,
userAgent: req.get('user-agent'),
};
if (isBatch) {
const primaryKeys = primaryKey.split(',');
const items = await Promise.all(primaryKeys.map(updateItem));
return res.json({ data: items || null });
} else {
const item = await updateItem(primaryKey);
return res.json({ data: item || null });
}
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
const updatedPrimaryKey = await ItemsService.updateItem(
req.collection,
primaryKey,
req.body,
accountability
);
async function updateItem(pk: string | number) {
const primaryKey = await ItemsService.updateItem(req.collection, pk, req.body, {
role: req.role,
admin: req.admin,
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
const result = await ItemsService.readItem(
req.collection,
updatedPrimaryKey,
req.sanitizedQuery,
accountability
);
const item = await ItemsService.readItem(
req.collection,
primaryKey,
req.sanitizedQuery,
{
role: req.role,
admin: req.admin,
}
);
return item;
}
res.json({ data: result || null });
})
);

View File

@@ -1,6 +1,5 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
import { Accountability } from '../types';
import { Accountability, Item, Query } from '../types';
export enum Action {
CREATE = 'create',
@@ -12,7 +11,7 @@ export enum Action {
AUTHENTICATE = 'authenticate',
}
export const createActivity = async (data: Record<string, any>) => {
export const createActivity = async (data: Partial<Item>) => {
return await ItemsService.createItem('directus_activity', data);
};
@@ -30,7 +29,7 @@ export const readActivity = async (
export const updateActivity = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
return await ItemsService.updateItem('directus_activity', pk, data, accountability);

View File

@@ -9,17 +9,17 @@ import parseEXIF from 'exif-reader';
import parseIPTC from '../utils/parse-iptc';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { Accountability } from '../types';
import { Accountability, Item } from '../types';
import { Readable } from 'stream';
export const createFile = async (
data: Record<string, any>,
data: Partial<Item>,
stream: NodeJS.ReadableStream,
accountability: Accountability
) => {
const id = uuidv4();
const payload: Record<string, any> = {
const payload: Partial<Item> = {
...data,
id,
};
@@ -70,7 +70,7 @@ export const readFile = async (
export const updateFile = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability,
stream?: NodeJS.ReadableStream
) => {
@@ -98,6 +98,7 @@ export const updateFile = async (
};
export const deleteFile = async (pk: string, accountability: Accountability) => {
/** @todo use ItemsService */
const file = await database
.select('storage', 'filename_disk')
.from('directus_files')

View File

@@ -1,8 +1,8 @@
import * as ItemsService from './items';
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
export const createFolder = async (
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
): Promise<string> => {
return (await ItemsService.createItem('directus_folders', data, accountability)) as string;
@@ -18,7 +18,7 @@ export const readFolder = async (pk: string, query: Query, accountability?: Acco
export const updateFolder = async (
pk: string,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
): Promise<string> => {
return (await ItemsService.updateItem('directus_folders', pk, data, accountability)) as string;

View File

@@ -2,21 +2,26 @@ import database, { schemaInspector } from '../database';
import { Query } from '../types/query';
import runAST from '../database/run-ast';
import getASTFromQuery from '../utils/get-ast-from-query';
import { Accountability, Operation } from '../types';
import { Accountability, Operation, Item } from '../types';
import * as PayloadService from './payload';
import * as PermissionsService from './permissions';
import * as ActivityService from './activity';
import * as RevisionsService from './revisions';
import { pick } from 'lodash';
import { pick, clone } from 'lodash';
import logger from '../logger';
/**
* @todo
* have this support passing in a knex instance, so we can hook it up to the same TX instance
* as batch insert / update
*/
async function saveActivityAndRevision(
action: ActivityService.Action,
collection: string,
item: string,
payload: Record<string, any>,
payload: Partial<Item>,
accountability: Accountability
) {
const activityID = await ActivityService.createActivity({
@@ -43,17 +48,17 @@ async function saveActivityAndRevision(
export async function createItem(
collection: string,
data: Record<string, any>[],
data: Partial<Item>[],
accountability?: Accountability
): Promise<(string | number)[]>;
export async function createItem(
collection: string,
data: Record<string, any>,
data: Partial<Item>,
accountability?: Accountability
): Promise<string | number>;
export async function createItem(
collection: string,
data: Record<string, any> | Record<string, any>[],
data: Partial<Item> | Partial<Item>[],
accountability?: Accountability
): Promise<string | number | (string | number)[]> {
const isBatch = Array.isArray(data);
@@ -62,7 +67,7 @@ export async function createItem(
let payloads = isBatch ? data : [data];
const primaryKeys: (string | number)[] = await Promise.all(
payloads.map(async (payload: Record<string, any>) => {
payloads.map(async (payload: Partial<Item>) => {
if (accountability && accountability.admin === false) {
payload = await PermissionsService.processValues(
'create',
@@ -116,7 +121,7 @@ export async function createItem(
});
}
export const readItems = async <T = Record<string, any>>(
export const readItems = async <T = Partial<Item>>(
collection: string,
query: Query,
accountability?: Accountability
@@ -131,27 +136,13 @@ export const readItems = async <T = Record<string, any>>(
return await PayloadService.processValues('read', collection, records);
};
export async function readItem<T = Record<string, any>>(
export const readItem = async <T extends number | string | (number | string)[]>(
collection: string,
pk: number | string,
query?: Query,
accountability?: Accountability,
operation?: Operation
): Promise<T>;
export async function readItem<T = Record<string, any>>(
collection: string,
pk: (number | string)[],
query?: Query,
accountability?: Accountability,
operation?: Operation
): Promise<T[]>;
export async function readItem<T = Record<string, any>>(
collection: string,
pk: number | string | (number | string)[],
pk: T,
query: Query = {},
accountability?: Accountability,
operation?: Operation
): Promise<T | T[]> {
): Promise<T extends number | string ? Partial<Item> : Partial<Item>[]> => {
// We allow overriding the operation, so we can use the item read logic to validate permissions
// for update and delete as well
operation = operation || 'read';
@@ -190,56 +181,71 @@ export async function readItem<T = Record<string, any>>(
const records = await runAST(ast);
const processedRecords = await PayloadService.processValues('read', collection, records);
return isBatch ? processedRecords : processedRecords[0];
}
};
export const updateItem = async (
export const updateItem = async <T extends number | string | (number | string)[]>(
collection: string,
pk: number | string,
data: Record<string, any>,
pk: T,
data: Partial<Item>,
accountability?: Accountability
): Promise<string | number> => {
let payload = data;
): Promise<T> => {
const primaryKeys: any[] = Array.isArray(pk) ? pk : [pk];
if (accountability && accountability.admin === false) {
await PermissionsService.checkAccess('update', collection, pk, accountability.role);
const updatedPrimaryKeys = database.transaction(async (tx) => {
let payload = clone(data);
payload = await PermissionsService.processValues(
'validate',
collection,
accountability.role,
data
return await Promise.all(
primaryKeys.map(async (key) => {
if (accountability && accountability.admin === false) {
await PermissionsService.checkAccess(
'update',
collection,
key,
accountability.role
);
payload = await PermissionsService.processValues(
'validate',
collection,
accountability.role,
data
);
}
payload = await PayloadService.processValues('update', collection, payload);
payload = await PayloadService.processM2O(collection, payload);
const primaryKeyField = await schemaInspector.primary(collection);
// Only insert the values that actually save to an existing column. This ensures we ignore aliases etc
const columns = await schemaInspector.columns(collection);
const payloadWithoutAlias = pick(
payload,
columns.map(({ column }) => column)
);
await tx(collection)
.update(payloadWithoutAlias)
.where({ [primaryKeyField]: key });
if (accountability) {
// Don't await this. It can run async in the background
saveActivityAndRevision(
ActivityService.Action.UPDATE,
collection,
String(pk),
payloadWithoutAlias,
accountability
).catch((err) => logger.error(err));
}
return pk;
})
);
}
});
payload = await PayloadService.processValues('update', collection, payload);
payload = await PayloadService.processM2O(collection, payload);
const primaryKeyField = await schemaInspector.primary(collection);
// Only insert the values that actually save to an existing column. This ensures we ignore aliases etc
const columns = await schemaInspector.columns(collection);
const payloadWithoutAlias = pick(
payload,
columns.map(({ column }) => column)
);
await database(collection)
.update(payloadWithoutAlias)
.where({ [primaryKeyField]: pk });
if (accountability) {
// Don't await this. It can run async in the background
saveActivityAndRevision(
ActivityService.Action.UPDATE,
collection,
String(pk),
payloadWithoutAlias,
accountability
).catch((err) => logger.error(err));
}
return pk;
return Array.isArray(pk) ? updatedPrimaryKeys : updatedPrimaryKeys[0];
};
export const deleteItem = async (
@@ -293,7 +299,7 @@ export const readSingleton = async (
export const upsertSingleton = async (
collection: string,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
const primaryKeyField = await schemaInspector.primary(collection);

View File

@@ -15,11 +15,7 @@ import * as ItemsService from './items';
type Operation = 'create' | 'read' | 'update';
type Transformers = {
[type: string]: (
operation: Operation,
value: any,
payload: Record<string, any>
) => Promise<any>;
[type: string]: (operation: Operation, value: any, payload: Partial<Item>) => Promise<any>;
};
/**
@@ -72,7 +68,7 @@ const transformers: Transformers = {
export const processValues = async (
operation: Operation,
collection: string,
payload: Record<string, any> | Record<string, any>[]
payload: Partial<Item> | Partial<Item>[]
) => {
let processedPayload = clone(payload);
@@ -113,7 +109,7 @@ export const processValues = async (
async function processField(
field: Pick<System, 'field' | 'special'>,
payload: Record<string, any>,
payload: Partial<Item>,
operation: Operation
) {
if (transformers.hasOwnProperty(field.special)) {
@@ -126,7 +122,7 @@ async function processField(
/**
* Recursively checks for nested relational items, and saves them bottom up, to ensure we have IDs etc ready
*/
export const processM2O = async (collection: string, payload: Record<string, any>) => {
export const processM2O = async (collection: string, payload: Partial<Item>) => {
const payloadClone = clone(payload);
const relations = await database
@@ -145,7 +141,7 @@ export const processM2O = async (collection: string, payload: Record<string, any
// Save all nested m2o records
await Promise.all(
relationsToProcess.map(async (relation) => {
const relatedRecord: Record<string, any> = payloadClone[relation.field_many];
const relatedRecord: Partial<Item> = payloadClone[relation.field_many];
const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.primary_one);
let relatedPrimaryKey: string | number;
@@ -172,7 +168,7 @@ export const processM2O = async (collection: string, payload: Record<string, any
return payloadClone;
};
export const processO2M = async (collection: string, payload: Record<string, any>) => {
export const processO2M = async (collection: string, payload: Partial<Item>) => {
const payloadClone = clone(payload);
const relations = await database
@@ -194,7 +190,7 @@ export const processO2M = async (collection: string, payload: Record<string, any
const relatedRecords = payloadClone[relation.field_one];
await Promise.all(
relatedRecords.map(async (relatedRecord: Record<string, any>, index: number) => {
relatedRecords.map(async (relatedRecord: Partial<Item>, index: number) => {
relatedRecord[relation.field_many] = payloadClone[relation.primary_one];
const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.primary_many);

View File

@@ -6,6 +6,7 @@ import {
Query,
Permission,
Operation,
Item,
} from '../types';
import * as ItemsService from './items';
import database from '../database';
@@ -14,7 +15,7 @@ import { uniq } from 'lodash';
import generateJoi from '../utils/generate-joi';
export const createPermission = async (
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
): Promise<number> => {
return (await ItemsService.createItem('directus_permissions', data, accountability)) as number;
@@ -30,7 +31,7 @@ export const readPermission = async (pk: number, query: Query, accountability?:
export const updatePermission = async (
pk: number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
): Promise<number> => {
return (await ItemsService.updateItem(
@@ -183,7 +184,7 @@ export const processValues = async (
operation: Operation,
collection: string,
role: string | null,
data: Record<string, any>
data: Partial<Item>
) => {
const permission = await database
.select<Permission>('*')

View File

@@ -1,10 +1,10 @@
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
import * as ItemsService from './items';
/** @todo check if we want to save activity for collection presets */
export const createCollectionPreset = async (
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
return await ItemsService.createItem('directus_presets', data, accountability);
@@ -24,7 +24,7 @@ export const readCollectionPreset = async (
export const updateCollectionPreset = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
return await ItemsService.updateItem('directus_presets', pk, data, accountability);

View File

@@ -1,7 +1,7 @@
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
import * as ItemsService from './items';
export const createRelation = async (data: Record<string, any>, accountability: Accountability) => {
export const createRelation = async (data: Partial<Item>, accountability: Accountability) => {
return await ItemsService.createItem('directus_relations', data, accountability);
};
@@ -19,7 +19,7 @@ export const readRelation = async (
export const updateRelation = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
return await ItemsService.updateItem('directus_relations', pk, data, accountability);

View File

@@ -1,7 +1,7 @@
import * as ItemsService from './items';
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
export const createRevision = async (data: Record<string, any>) => {
export const createRevision = async (data: Partial<Item>) => {
return await ItemsService.createItem('directus_revisions', data);
};

View File

@@ -1,7 +1,7 @@
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
import * as ItemsService from './items';
export const createRole = async (data: Record<string, any>, accountability: Accountability) => {
export const createRole = async (data: Partial<Item>, accountability: Accountability) => {
return await ItemsService.createItem('directus_roles', data, accountability);
};
@@ -19,7 +19,7 @@ export const readRole = async (
export const updateRole = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
return await ItemsService.updateItem('directus_roles', pk, data, accountability);

View File

@@ -1,4 +1,4 @@
import { Query } from '../types/query';
import { Query, Item } from '../types/query';
import * as ItemsService from './items';
import { Accountability } from '../types';
@@ -6,6 +6,6 @@ export const readSettings = async (query: Query, accountability?: Accountability
return await ItemsService.readSingleton('directus_settings', query, accountability);
};
export const updateSettings = async (data: Record<string, any>, accountability: Accountability) => {
export const updateSettings = async (data: Partial<Item>, accountability: Accountability) => {
return await ItemsService.upsertSingleton('directus_settings', data, accountability);
};

View File

@@ -4,9 +4,9 @@ import { sendInviteMail } from '../mail';
import database from '../database';
import argon2 from 'argon2';
import { InvalidPayloadException } from '../exceptions';
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
export const createUser = async (data: Record<string, any>, accountability: Accountability) => {
export const createUser = async (data: Partial<Item>, accountability: Accountability) => {
return await ItemsService.createItem('directus_users', data, accountability);
};
@@ -24,7 +24,7 @@ export const readUser = async (
export const updateUser = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
/**

View File

@@ -1,7 +1,7 @@
import { Accountability, Query } from '../types';
import { Accountability, Query, Item } from '../types';
import * as ItemsService from './items';
export const createWebhook = async (data: Record<string, any>, accountability: Accountability) => {
export const createWebhook = async (data: Partial<Item>, accountability: Accountability) => {
return await ItemsService.createItem('directus_webhooks', data, accountability);
};
@@ -19,7 +19,7 @@ export const readWebhook = async (
export const updateWebhook = async (
pk: string | number,
data: Record<string, any>,
data: Partial<Item>,
accountability: Accountability
) => {
return await ItemsService.updateItem('directus_webhooks', pk, data, accountability);

View File

@@ -3,6 +3,7 @@
*/
import { Permission } from './permissions';
import { Query } from './query';
export {};
@@ -14,7 +15,7 @@ declare global {
role: string | null;
admin: boolean;
collection?: string;
sanitizedQuery?: Record<string, any>;
sanitizedQuery?: Query;
single?: boolean;
permissions?: Permission;
}

View File

@@ -9,3 +9,4 @@ export * from './permissions';
export * from './query';
export * from './relation';
export * from './sessions';
export * from './items';

6
src/types/items.ts Normal file
View File

@@ -0,0 +1,6 @@
/**
* I know this looks a little silly, but it allows us to explicitly differentiate between when we're
* expecting an item vs any other generic object.
*/
export type Item = Record<string, any>;