feathers-fletching
Version:
Hooks, services, and plugins for feathers.js
167 lines (150 loc) • 4.46 kB
text/typescript
import type { HookContext } from '@feathersjs/feathers';
import type { MaybeArray, Promisable } from './utils';
import { isPromise } from './utils';
// Note we try to avoid adding promises to the event loop when not
// neccessary by using `typeof result.then` instead of just
// await Promise.resolve(virtuals[key](updated, context, prepResult))
// The resolve funtion used for withData, withQuery and withResult.
// This function iterates the keys of the `virtuals` and assigns
// the value to that key as the result of some value or function.
/*
{
thing: 1, // return some primitive such as a number, bool, obj, string, etc
thingFunc: (item, context, prepResult) => {
// Return the result of a function that was give the args
// item, the whole context, and the result of the prepFunction
return item + context.params.itemToAdd
},
users: (item, context, prepResult) => {
// Return a promise
return context.app.service('users').get(item.user_id)
}
}
*/
type ResolverFn = typeof resolver;
// Mutate the data at updated[key] in place according to its virtual.
export const resolver = (
virtual: VirtualFn,
key: string,
updated: any,
context: HookContext,
prepResult: any
) => {
if (typeof virtual === 'function') {
const result = virtual(updated, context, prepResult);
if (isPromise(result)) {
return result.then((result) => {
if (typeof result !== 'undefined') {
updated[key] = result;
}
});
}
if (typeof result !== 'undefined') {
updated[key] = result;
return result;
}
return result;
} else {
updated[key] = virtual;
return virtual;
}
};
// This serializer is a bit different from the other resolve
// because it does append the result to the object even if the
// function returned `undefined`. This is because it is used
// as a "filter" where each function result returns a truth/falsy
// value indicating if it should be filtered, and `undefined` is falsy
// and should be respected as a valid returned value
export const filterResolver = (
virtual: any,
key: string,
updated: any,
context: HookContext,
prepResult: any
) => {
if (typeof virtual === 'function') {
const result = virtual(updated, context, prepResult);
if (isPromise(result)) {
return result.then((shouldKeep) => {
if (!shouldKeep) {
delete updated[key];
}
});
} else {
if (!result) {
delete updated[key];
}
return result;
}
} else {
if (!virtual) {
delete updated[key];
}
return virtual;
}
};
const serializer = async (
item: Record<string, any>,
virtuals: Virtuals,
context: HookContext,
prepResult: PrepFunction,
resolver: ResolverFn
) => {
const updated = Object.assign({}, item);
const syncKeys = [];
const asyncKeys = [];
Object.keys(virtuals).forEach((key) => {
(key.startsWith('@') ? syncKeys : asyncKeys).push(key);
});
if (syncKeys.length) {
for (const key of syncKeys) {
const result = resolver(
virtuals[key],
key.substring(1),
updated,
context,
prepResult
);
if (isPromise(result)) {
await result;
}
}
}
if (asyncKeys.length) {
const results = asyncKeys.map((key) => {
return resolver(virtuals[key], key, updated, context, prepResult);
});
if (results.some((result) => isPromise(result))) {
await Promise.all(results.filter((result) => isPromise(result)));
}
}
return updated;
};
export const virtualsSerializer = async (
resolver: ResolverFn,
data: MaybeArray<Record<string, any>>,
virtuals: Virtuals,
context: HookContext,
// eslint-disable-next-line @typescript-eslint/no-empty-function
prepFunc: PrepFunction = () => {}
) => {
let prepResult = prepFunc(context);
if (isPromise(prepResult)) {
prepResult = await prepResult.then((result) => result);
}
if (Array.isArray(data)) {
return Promise.all(
data.map((item) =>
serializer(item, virtuals, context, prepResult, resolver)
)
);
}
return serializer(data, virtuals, context, prepResult, resolver);
};
export type VirtualFn = (
data: Record<string, any>,
context: HookContext,
prepResult: Record<string, any>
) => any;
export type Virtuals = Record<string, VirtualFn>;
export type PrepFunction = (context: HookContext) => Promisable<any>;