import env from '../util/env.js'; import $msg from 'message-tag'; import $uri from 'uri-tag'; import merge, { Merge } from '../util/merge.js'; import * as ObjectUtil from '../util/ObjectUtil.js'; import { concatUri } from '../util/uri.js'; import { Schema, DecodeError } from '../schema/Schema.js'; import * as t from 'io-ts'; import { AxiosResponse } from 'axios'; import type { ResourcePath, URI, StorePath, Agent, Context, ResourceDefinition, Resource, ResourceCreator, ResourceSpec } from './Resource.js'; import { resourceDef, intantiateSpec } from './Resource.js'; import Adapter, { AdapterT } from './Adapter.js'; import { StorablePromise, makeStorable } from './StorablePromise.js'; export type ItemSchema = Schema; export type ItemResourceSpec = ResourceSpec; // Generic collection resource type (i.e. as general as we can define it without knowing the actual spec) export type ItemResourceT = Resource; //& { [resourceDef] : { spec : {} } }; // Can extend, if needed export type DeleteResponse = { response : unknown }; const defaultMethods = { async head(this : ItemResourceT, params = {}) : Promise { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.head(spec.uri, { params }); return response; }, async get(this : ItemResourceT, params = {}) : Promise> { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.get(spec.uri, { params }); const instance = adapter.decode(adapter.parse(response)); return instance; }, async put(this : ItemResourceT, instance : unknown, params = {}) : Promise> { const { agent, schema, adapter, ...spec } = this[resourceDef]; const instanceEncoded = adapter.encode(instance); const response = await agent.put(spec.uri, instanceEncoded, { params }); return adapter.report(schema.decode(adapter.parse(response))); }, async patch(this : ItemResourceT, instance : unknown, params = {}) : Promise> { const { agent, schema, adapter, ...spec } = this[resourceDef]; const schemaPartial = adapter.partial(); const instanceEncoded = schemaPartial.encode(instance); const response = await agent.patch(spec.uri, instanceEncoded, { params }); return adapter.report(schema.decode(adapter.parse(response))); }, async delete(this : ItemResourceT, instanceEncoded : unknown, params = {}) : Promise { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.delete(spec.uri, { params }); const responseData = response.data; const responseParsed = typeof responseData === 'string' && responseData.trim() === '' ? undefined : responseData; return { response: responseParsed, }; }, async post(this : ItemResourceT, body : unknown, params = {}) : Promise { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.post(spec.uri, { params }); return response.data; }, }; const itemDefaults = { path: [], uri: '', store: [], methods: { head: defaultMethods.head, get(this : ItemResourceT, params = {}) : StorablePromise> { return makeStorable(Function.prototype.call.call(defaultMethods.get, this, params), { location: this[resourceDef].store, operation: 'put', }); }, put(this : ItemResourceT, instance : unknown, params = {}) : StorablePromise> { return makeStorable(Function.prototype.call.call(defaultMethods.put, this, instance, params), { location: this[resourceDef].store, operation: 'put', }); }, patch(this : ItemResourceT, instance : unknown, params = {}) : StorablePromise> { return makeStorable(Function.prototype.call.call(defaultMethods.patch, this, instance, params), { location: this[resourceDef].store, operation: 'put', }); }, delete(this : ItemResourceT, instanceEncoded : unknown, params = {}) : StorablePromise { return makeStorable( Function.prototype.call.call(defaultMethods.delete, this, instanceEncoded, params), { location: this[resourceDef].store, operation: 'clear', }, ); }, post(this : ItemResourceT, body : unknown, params = {}) : StorablePromise { return makeStorable(Function.prototype.call.call(defaultMethods.post, this, body, params), { location: this[resourceDef].store, operation: 'put', }); }, }, resources: {}, }; export const ItemResource = >>( schema : S, itemSpec : Spec = {} as Spec ) => { // Utility types type MethodsFromSpec['methods']> = { [key in keyof M] : M[key] extends (...args : infer A) => infer R ? (...args : A) => R : never }; type ResourcesFromSpec['resources']> = { [key in keyof R] : R[key] extends (context : Context) => infer R ? R : never }; // The interface of the resource, split up into its base components type ResourceComponents = { methods : Merge<(typeof itemDefaults)['methods'], MethodsFromSpec>, resources : ResourcesFromSpec, }; // The interface of the resource, after merging the different components and adding context information type ItemResource = Resource & ResourceComponents['methods'] & ResourceComponents['resources']; const makeResource = (context : Context) : ItemResource => { const spec : ItemResourceSpec = intantiateSpec(context, itemSpec, itemDefaults); // Get methods and subresources const methods = spec.methods as ResourceComponents['methods']; const resources = spec.resources as ResourceComponents['resources']; // Make sure there's no `resourceDef` key included (should not be overridden) if (resourceDef in methods || resourceDef in resources) { throw new TypeError($msg`Cannot override resourceDef key`); } const resourceDefinition : ResourceDefinition = { agent: context.agent, options: context.options, ...spec, schema, methods: itemDefaults.methods, adapter: null as unknown as AdapterT, }; resourceDefinition.adapter = context.options.adapter(resourceDefinition, schema); const resource : ItemResource = { ...methods, ...resources, [resourceDef]: resourceDefinition, }; return resource; }; return Object.assign(makeResource, { schema, // Expose the schema on the constructor }); }; export default ItemResource;