@data-client/rest
Version:
Quickly define typed REST resources and endpoints
201 lines (193 loc) • 5.96 kB
text/typescript
import { schema } from '@data-client/endpoint';
import type { SnapshotInterface, Queryable } from '@data-client/endpoint';
import {
ResourceGenerics,
ResourceOptions,
Resource,
ResourceInterface,
} from './resourceTypes.js';
import RestEndpoint, {
GetEndpoint,
PartialRestGenerics,
RestEndpointOptions,
RestInstanceBase,
} from './RestEndpoint.js';
import { shortenPath } from './RestHelpers.js';
const { Invalidate, Collection: BaseCollection } = schema;
/** Creates collection of Endpoints for common operations on a given data/schema.
*
* @see https://dataclient.io/rest/api/resource
*/
export default function resource<O extends ResourceGenerics>({
path,
schema,
Endpoint = RestEndpoint,
Collection = BaseCollection,
optimistic,
paginationField,
...extraOptions
}: Readonly<O> & ResourceOptions): Resource<O> {
if (process.env.NODE_ENV !== 'production') {
// if they lowercase and it looks like they meant to use upper-case version
if (
'endpoint' in extraOptions &&
Endpoint === RestEndpoint &&
typeof extraOptions['endpoint'] === 'function' &&
extraOptions['endpoint'] &&
Object.prototype.isPrototypeOf.call(
RestEndpoint.prototype,
(extraOptions['endpoint'] as any).prototype,
)
) {
console.warn(
`You passed 'endpoint' option; did you mean to use Endpoint?
https://dataclient.io/rest/api/resource#endpoint
This parameter must be capitalized.
This warning will not show in production.`,
);
}
// if they lowercase and it looks like they meant to use upper-case version
if (
'collection' in extraOptions &&
Collection === BaseCollection &&
typeof extraOptions['collection'] === 'function' &&
extraOptions['collection'] &&
Object.prototype.isPrototypeOf.call(
BaseCollection.prototype,
(extraOptions['collection'] as any).prototype,
)
) {
console.warn(
`You passed 'collection' option; did you mean to use Collection?
https://dataclient.io/rest/api/resource#collection
This parameter must be capitalized.
This warning will not show in production.`,
);
}
}
const shortenedPath = shortenPath(path);
const getName = (name: string) => `${(schema as any)?.name}.${name}`;
// this accounts for derivative endpoints
function extendMember(extended: any, key: string, options: any) {
extended[key] = extended[key].extend(options);
}
const extraMutateOptions = { ...extraOptions };
const extraPartialOptions = { ...extraOptions };
const get: GetEndpoint<{ path: O['path']; schema: O['schema'] }> =
new Endpoint({
...extraOptions,
path,
schema,
name: getName('get'),
}) as any;
if (optimistic) {
(extraMutateOptions as any).getOptimisticResponse = optimisticUpdate;
// TODO: Check that schema is a queryable, otherwise this doesn't make sense
(extraPartialOptions as any).getOptimisticResponse = optimisticPartial(
schema as any,
);
}
const getList = new Endpoint({
...extraMutateOptions,
paginationField: paginationField as string,
path: shortenedPath,
schema: new Collection([schema as any]),
name: getName('getList'),
});
const ret = {
get,
getList,
// TODO(deprecated): remove this once we remove creates
create: getList.push.extend({ name: getName('create') }),
update: new Endpoint({
...extraMutateOptions,
path,
schema,
method: 'PUT',
name: getName('update'),
}),
partialUpdate: new Endpoint({
...extraPartialOptions,
path,
schema,
method: 'PATCH',
name: getName('partialUpdate'),
}),
delete: new Endpoint({
...extraMutateOptions,
path,
schema: (schema as any).process ? new Invalidate(schema as any) : schema,
method: 'DELETE',
name: getName('delete'),
process(res: any, params: any) {
return res && Object.keys(res).length ? res : params;
},
getOptimisticResponse: optimistic ? (optimisticDelete as any) : undefined,
}),
extend(
...args:
| [string, RestEndpointOptions & PartialRestGenerics]
| [RestEndpointOptions & PartialRestGenerics]
| [
(
baseResource: ResourceInterface,
) => Record<string, RestInstanceBase>,
]
) {
if (typeof args[0] === 'string') {
const [key, options] = args;
if (key in this) {
const extended = { ...this };
extendMember(extended, key, options);
return extended;
} else {
return {
...this,
[key]: this.get.extend(options),
};
}
} else if (typeof args[0] === 'function') {
const extended = args[0](this);
return {
...this,
...extended,
};
}
const overrides = args[0];
const extended = { ...this };
for (const key in overrides) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
extendMember(extended, key, overrides[key]);
}
return extended;
},
} as any;
return ret;
}
function optimisticUpdate(snap: SnapshotInterface, params: any, body: any) {
return {
...params,
...ensurePojo(body),
};
}
function optimisticPartial(schema: Queryable) {
return function (snap: SnapshotInterface, params: any, body: any) {
const data = snap.get(schema, params);
if (!data) throw snap.abort;
return {
...params,
...data,
// even tho we don't always have two arguments, the extra one will simply be undefined which spreads fine
...ensurePojo(body),
};
};
}
function optimisticDelete(snap: SnapshotInterface, params: any) {
return params;
}
function ensurePojo(body: any) {
return body instanceof FormData ?
Object.fromEntries((body as any).entries())
: body;
}