import $msg from 'message-tag'; import env from '../util/env.js'; import merge, { Merge } from '../util/merge.js'; import * as ObjectUtil from '../util/ObjectUtil.js'; import $uri from 'uri-tag'; import { concatUri } from '../util/uri.js'; import { Schema, DecodeError } from '../schema/Schema.js'; import * as t from 'io-ts'; import { AxiosResponse } from 'axios'; import * as Location from './Location.js'; 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 CollSchema = Schema; export type CollResourceSpec = ResourceSpec & { getKey : (value : t.TypeOf) => unknown, entry : ResourceCreator, }; // Generic collection resource type (i.e. as general as we can define it without knowing the actual spec) export type CollResourceT = Resource & { (index : Location.Index) : Resource, entrySchema : Schema, }; const defaultMethods = { async head(this : CollResourceT, params = {}) : Promise { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.head(spec.uri, { params }); return response; }, async get(this : CollResourceT, params = {}) : Promise> { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.get(spec.uri, { params }); return adapter.decode(adapter.parse(response)); }, // Alias for `get` async list(this : CollResourceT, params = {}) : Promise> { return await collectionDefaults.methods.get.call(this, params); }, /* TODO async put(this : CollResourceT, instance : unknown, params = {}) : Promise> { // }, async patch(this : CollResourceT, instance : unknown, params = {}) : Promise> { const { agent, schema, adapter, ...spec } = this[resourceDef]; const schemaPartial = adapter.partial(); const instanceEncoded = schema.encode(instance); const response = await agent.patch(spec.uri, instanceEncoded, { params }); return adapter.report(schema.decode(adapter.parse(response))); }, async delete(this : CollResourceT, instanceEncoded : unknown, params = {}) : Promise { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.delete(spec.uri, { params }); return response.data; }, */ // Generic POST (does not assume anything about the data model) async post(this : CollResourceT, instance : unknown, params = {}) : Promise { const { agent, schema, adapter, ...spec } = this[resourceDef]; const response = await agent.post(spec.uri, { params }); return response; }, async create(this : CollResourceT, instance : unknown, params = {}) : Promise> { const { agent, schema, adapter, ...spec } = this[resourceDef]; //const entrySchema = this.entrySchema; // FIXME const entrySchema = (this[resourceDef] as any).entry.schema; const entryAdapter = adapter.with(entrySchema); const instanceEncoded = entryAdapter.encode(instance); const response = await agent.post(spec.uri, instanceEncoded, { params }); const entryResult : t.TypeOf = entryAdapter.report( entrySchema.decode(entryAdapter.parse(response)) ); return entryResult; }, }; const collectionDefaults = { path: [], uri: '', store: [], methods: { head: defaultMethods.head, // Alias for `get` list(this : CollResourceT, params = {}) : StorablePromise> { return makeStorable(Function.prototype.call.call(defaultMethods.get, this, params), { location: this[resourceDef].store, operation: 'put', }); }, get(this : CollResourceT, params = {}) : StorablePromise> { return makeStorable(Function.prototype.call.call(defaultMethods.get, this, params), { location: this[resourceDef].store, operation: 'put', }); }, post: defaultMethods.post, create(this : CollResourceT, instance : unknown, params = {}) : StorablePromise> { const { schema, adapter, store } = this[resourceDef]; return makeStorable(Function.prototype.call.call(defaultMethods.create, this, instance, params), { location: (result : unknown) => { if (typeof result === 'undefined') { return [...store, undefined] as unknown as Location.Location; // FIXME } try { // const schemaKey = adapter.keyOf(schema); // const index = adapter.with(schemaKey).decode(result); // @ts-ignore const index = this[resourceDef].getKey(result); if (typeof index === 'undefined') { throw new TypeError(`Key does not exist on ${result}`); } return [...store, { index }]; } catch (e) { throw new TypeError($msg`Cannot get key of result: ${result}, reason: ${e}`); } }, operation: 'put', }); }, }, resources: {}, getKey: () => { throw new TypeError(`Unknown key type`); }, entry: (index : Location.Index) => { throw new TypeError($msg`Cannot construct entry`); }, }; export const CollectionResource = >>( schema : S, collSpec : 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 }; type EntryFromSpec['entry']> = E extends (context : Context) => infer R ? (index : Location.Index) => R : never; // The interface of the resource, split up into its base components type ResourceComponents = { methods : Merge<(typeof collectionDefaults)['methods'], MethodsFromSpec>, resources : ResourcesFromSpec, entry : Spec['entry'] extends Function ? EntryFromSpec : {}, }; // The interface of the resource, after merging the different components and adding context information type CollResource = Resource & ResourceComponents['methods'] & ResourceComponents['resources'] & ResourceComponents['entry']; const makeResource = (context : Context) : CollResource => { type SpecInstantiated = Omit, 'methods' | 'resources' | 'entry'> & { methods : ResourceComponents['methods'], resources : ResourceComponents['resources'], entry : Spec['entry'], }; const spec = intantiateSpec(context, collSpec, collectionDefaults) as SpecInstantiated; // 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 entry = spec.entry; const makeEntry = ((index : Location.Index) => { const entryContext = { options: context.options, agent: context.agent, path: [...spec.path, { index }], store: [...spec.store, { index }], uri: concatUri([spec.uri, Location.indexAsString(index)]), }; return (entry as any)(entryContext); }) as ResourceComponents['entry']; const resourceDefinition : ResourceDefinition = { agent: context.agent, options: context.options, ...spec, schema, methods: collectionDefaults.methods, adapter: null as unknown as AdapterT, }; resourceDefinition.adapter = context.options.adapter(resourceDefinition, schema); // @ts-ignore resourceDefinition.getKey = spec.getKey; const resource : CollResource = Object.assign(makeEntry, methods, resources, { [resourceDef]: resourceDefinition, }, ); return resource; }; return Object.assign(makeResource, { schema, // Expose the schema on the constructor }); }; export default CollectionResource;