|
- import * as v from 'valibot';
- import {PatchContentType} from './media-type';
- import {DataSource, ResourceIdConfig} from '../backend';
-
- export const CAN_PATCH_VALID_VALUES = ['merge', 'delta'] as const;
-
- export type CanPatchSpec = typeof CAN_PATCH_VALID_VALUES[number];
-
- export const PATCH_CONTENT_MAP_TYPE: Record<PatchContentType, CanPatchSpec> = {
- 'application/merge-patch+json': 'merge',
- 'application/json-patch+json': 'delta',
- };
-
- type CanPatchObject = Record<CanPatchSpec, boolean>;
-
- export interface Relationship<SubjectSchema extends v.BaseSchema, ObjectSchema extends v.BaseSchema> {
- objectResource: Resource<BaseResourceType & { schema: ObjectSchema }>,
- name: string;
- // points to object ID
- subjectAttr: string;
- }
-
- export interface ResourceState<
- ItemName extends string = string,
- RouteName extends string = string
- > {
- shared: Map<string, unknown>;
- relationships: Map<string, Relationship<any, any>>;
- itemName: ItemName;
- routeName: RouteName;
- canCreate: boolean;
- canFetchCollection: boolean;
- canFetchItem: boolean;
- canPatch: CanPatchObject;
- canEmplace: boolean;
- canDelete: boolean;
- }
-
- type CanPatch = boolean | Partial<CanPatchObject> | CanPatchSpec[];
-
- export interface BaseResourceType {
- schema: v.BaseSchema;
- name: string;
- routeName: string;
- idAttr: string;
- idSchema: v.BaseSchema;
- createdAtAttr: string;
- updatedAtAttr: string;
- }
-
- export interface Resource<ResourceType extends BaseResourceType = BaseResourceType> {
- schema: ResourceType['schema'];
- state: ResourceState<ResourceType['name'], ResourceType['routeName']>;
- name<NewName extends ResourceType['name']>(n: NewName): Resource<ResourceType & { name: NewName }>;
- route<NewRouteName extends ResourceType['routeName']>(n: NewRouteName): Resource<ResourceType & { routeName: NewRouteName }>;
- canFetchCollection(b?: boolean): this;
- canFetchItem(b?: boolean): this;
- canCreate(b?: boolean): this;
- canPatch(b?: CanPatch): this;
- canEmplace(b?: boolean): this;
- canDelete(b?: boolean): this;
- relatesTo<RelatedSchema extends v.BaseSchema>(
- resource: Resource<ResourceType & { schema: RelatedSchema }>,
- relationshipParams: Relationship<ResourceType['schema'], RelatedSchema>,
- ): this;
- dataSource?: DataSource;
- id<NewIdAttr extends ResourceType['idAttr'], TheIdSchema extends ResourceType['idSchema']>(
- newIdAttr: NewIdAttr,
- params: ResourceIdConfig<TheIdSchema>
- ): Resource<ResourceType & { idAttr: NewIdAttr, idSchema: TheIdSchema }>;
- addMetadata(id: string, value: unknown): this;
- setMetadata(id: string, value: unknown): this;
- createdAt<NewCreatedAtAttr extends ResourceType['createdAtAttr']>(n: NewCreatedAtAttr): Resource<ResourceType & { createdAtAttr: NewCreatedAtAttr }>;
- updatedAt<NewUpdatedAtAttr extends ResourceType['updatedAtAttr']>(n: NewUpdatedAtAttr): Resource<ResourceType & { updatedAtAttr: NewUpdatedAtAttr }>;
- }
-
- export const resource = <ResourceType extends BaseResourceType = BaseResourceType>(schema: ResourceType['schema']): Resource<ResourceType> => {
- const resourceState = {
- shared: new Map(),
- relationships: new Map<string, Relationship<any, any>>(),
- canCreate: false,
- canFetchCollection: false,
- canFetchItem: false,
- canPatch: {
- merge: false,
- delta: false,
- },
- canEmplace: false,
- canDelete: false,
- } as ResourceState<ResourceType['name'], ResourceType['routeName']>;
-
- return {
- get state(): ResourceState<ResourceType['name'], ResourceType['routeName']> {
- return Object.freeze({
- ...resourceState,
- });
- },
- canFetchCollection(b = true) {
- resourceState.canFetchCollection = b;
- return this;
- },
- canFetchItem(b = true) {
- resourceState.canFetchItem = b;
- return this;
- },
- canCreate(b = true) {
- resourceState.canCreate = b;
- return this;
- },
- canPatch(b = true as CanPatch) {
- if (typeof b === 'boolean') {
- resourceState.canPatch.merge = b;
- resourceState.canPatch.delta = b;
- return this;
- }
-
- if (typeof b === 'object') {
- if (Array.isArray(b)) {
- CAN_PATCH_VALID_VALUES.forEach((p) => {
- resourceState.canPatch[p] = b.includes(p);
- });
- return this;
- }
- if (b !== null) {
- CAN_PATCH_VALID_VALUES.forEach((p) => {
- resourceState.canPatch[p] = b[p] ?? false;
- });
- }
- }
-
- return this;
- },
- canEmplace(b = true) {
- resourceState.canEmplace = b;
- return this;
- },
- canDelete(b = true) {
- resourceState.canDelete = b;
- return this;
- },
- id(idName, config) {
- resourceState.shared.set('idAttr', idName);
- resourceState.shared.set('idConfig', config);
- return this;
- },
- addMetadata(key: string, value: unknown) {
- const fullTextAttrs = (resourceState.shared.get(key) ?? new Set()) as Set<unknown>;
- fullTextAttrs.add(value);
- this.setMetadata(key, fullTextAttrs);
- return this;
- },
- setMetadata(key: string, value: unknown) {
- resourceState.shared.set(key, value);
- return this;
- },
- name<NewName extends ResourceType['name']>(n: NewName) {
- resourceState.itemName = n;
- return this;
- },
- route<NewRouteName extends ResourceType['routeName']>(n: NewRouteName) {
- resourceState.routeName = n;
- return this;
- },
- get itemName() {
- return resourceState.itemName;
- },
- get routeName() {
- return resourceState.routeName;
- },
- get schema() {
- return schema;
- },
- relatesTo<RelatedSchema extends v.BaseSchema>(
- objectResource: Resource<ResourceType & { schema: RelatedSchema }>,
- relationshipParams: Relationship<ResourceType['schema'], RelatedSchema>,
- ) {
- resourceState.relationships.set(relationshipParams.name, {
- ...relationshipParams,
- objectResource,
- });
- return this;
- },
- createdAt<NewCreatedAtAttr extends ResourceType['createdAtAttr']>(n: NewCreatedAtAttr) {
- resourceState.shared.set('createdAtAttr', n);
- return this;
- },
- updatedAt<NewUpdatedAtAttr extends ResourceType['updatedAtAttr']>(n: NewUpdatedAtAttr) {
- resourceState.shared.set('updatedAtAttr', n);
- return this;
- },
- } as Resource<ResourceType>;
- };
-
- export type ResourceType<R extends Resource> = v.Output<R['schema']>;
-
- export const getAcceptPatchString = (canPatch: CanPatchObject) => {
- const validPatchTypes = Object.entries(canPatch)
- .filter(([, allowed]) => allowed)
- .map(([patchType]) => patchType);
-
- return Object.entries(PATCH_CONTENT_MAP_TYPE)
- .filter(([, patchType]) => validPatchTypes.includes(patchType))
- .map(([contentType ]) => contentType)
- .join(',');
- }
|