HATEOAS-first backend framework.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

206 wiersze
6.2 KiB

  1. import * as v from 'valibot';
  2. import {PatchContentType} from './media-type';
  3. import {DataSource, ResourceIdConfig} from '../backend';
  4. export const CAN_PATCH_VALID_VALUES = ['merge', 'delta'] as const;
  5. export type CanPatchSpec = typeof CAN_PATCH_VALID_VALUES[number];
  6. export const PATCH_CONTENT_MAP_TYPE: Record<PatchContentType, CanPatchSpec> = {
  7. 'application/merge-patch+json': 'merge',
  8. 'application/json-patch+json': 'delta',
  9. };
  10. type CanPatchObject = Record<CanPatchSpec, boolean>;
  11. export interface Relationship<SubjectSchema extends v.BaseSchema, ObjectSchema extends v.BaseSchema> {
  12. objectResource: Resource<BaseResourceType & { schema: ObjectSchema }>,
  13. name: string;
  14. // points to object ID
  15. subjectAttr: string;
  16. }
  17. export interface ResourceState<
  18. ItemName extends string = string,
  19. RouteName extends string = string
  20. > {
  21. shared: Map<string, unknown>;
  22. relationships: Map<string, Relationship<any, any>>;
  23. itemName: ItemName;
  24. routeName: RouteName;
  25. canCreate: boolean;
  26. canFetchCollection: boolean;
  27. canFetchItem: boolean;
  28. canPatch: CanPatchObject;
  29. canEmplace: boolean;
  30. canDelete: boolean;
  31. }
  32. type CanPatch = boolean | Partial<CanPatchObject> | CanPatchSpec[];
  33. export interface BaseResourceType {
  34. schema: v.BaseSchema;
  35. name: string;
  36. routeName: string;
  37. idAttr: string;
  38. idSchema: v.BaseSchema;
  39. createdAtAttr: string;
  40. updatedAtAttr: string;
  41. }
  42. export interface Resource<ResourceType extends BaseResourceType = BaseResourceType> {
  43. schema: ResourceType['schema'];
  44. state: ResourceState<ResourceType['name'], ResourceType['routeName']>;
  45. name<NewName extends ResourceType['name']>(n: NewName): Resource<ResourceType & { name: NewName }>;
  46. route<NewRouteName extends ResourceType['routeName']>(n: NewRouteName): Resource<ResourceType & { routeName: NewRouteName }>;
  47. canFetchCollection(b?: boolean): this;
  48. canFetchItem(b?: boolean): this;
  49. canCreate(b?: boolean): this;
  50. canPatch(b?: CanPatch): this;
  51. canEmplace(b?: boolean): this;
  52. canDelete(b?: boolean): this;
  53. relatesTo<RelatedSchema extends v.BaseSchema>(
  54. resource: Resource<ResourceType & { schema: RelatedSchema }>,
  55. relationshipParams: Relationship<ResourceType['schema'], RelatedSchema>,
  56. ): this;
  57. dataSource?: DataSource;
  58. id<NewIdAttr extends ResourceType['idAttr'], TheIdSchema extends ResourceType['idSchema']>(
  59. newIdAttr: NewIdAttr,
  60. params: ResourceIdConfig<TheIdSchema>
  61. ): Resource<ResourceType & { idAttr: NewIdAttr, idSchema: TheIdSchema }>;
  62. addMetadata(id: string, value: unknown): this;
  63. setMetadata(id: string, value: unknown): this;
  64. createdAt<NewCreatedAtAttr extends ResourceType['createdAtAttr']>(n: NewCreatedAtAttr): Resource<ResourceType & { createdAtAttr: NewCreatedAtAttr }>;
  65. updatedAt<NewUpdatedAtAttr extends ResourceType['updatedAtAttr']>(n: NewUpdatedAtAttr): Resource<ResourceType & { updatedAtAttr: NewUpdatedAtAttr }>;
  66. }
  67. export const resource = <ResourceType extends BaseResourceType = BaseResourceType>(schema: ResourceType['schema']): Resource<ResourceType> => {
  68. const resourceState = {
  69. shared: new Map(),
  70. relationships: new Map<string, Relationship<any, any>>(),
  71. canCreate: false,
  72. canFetchCollection: false,
  73. canFetchItem: false,
  74. canPatch: {
  75. merge: false,
  76. delta: false,
  77. },
  78. canEmplace: false,
  79. canDelete: false,
  80. } as ResourceState<ResourceType['name'], ResourceType['routeName']>;
  81. return {
  82. get state(): ResourceState<ResourceType['name'], ResourceType['routeName']> {
  83. return Object.freeze({
  84. ...resourceState,
  85. });
  86. },
  87. canFetchCollection(b = true) {
  88. resourceState.canFetchCollection = b;
  89. return this;
  90. },
  91. canFetchItem(b = true) {
  92. resourceState.canFetchItem = b;
  93. return this;
  94. },
  95. canCreate(b = true) {
  96. resourceState.canCreate = b;
  97. return this;
  98. },
  99. canPatch(b = true as CanPatch) {
  100. if (typeof b === 'boolean') {
  101. resourceState.canPatch.merge = b;
  102. resourceState.canPatch.delta = b;
  103. return this;
  104. }
  105. if (typeof b === 'object') {
  106. if (Array.isArray(b)) {
  107. CAN_PATCH_VALID_VALUES.forEach((p) => {
  108. resourceState.canPatch[p] = b.includes(p);
  109. });
  110. return this;
  111. }
  112. if (b !== null) {
  113. CAN_PATCH_VALID_VALUES.forEach((p) => {
  114. resourceState.canPatch[p] = b[p] ?? false;
  115. });
  116. }
  117. }
  118. return this;
  119. },
  120. canEmplace(b = true) {
  121. resourceState.canEmplace = b;
  122. return this;
  123. },
  124. canDelete(b = true) {
  125. resourceState.canDelete = b;
  126. return this;
  127. },
  128. id(idName, config) {
  129. resourceState.shared.set('idAttr', idName);
  130. resourceState.shared.set('idConfig', config);
  131. return this;
  132. },
  133. addMetadata(key: string, value: unknown) {
  134. const fullTextAttrs = (resourceState.shared.get(key) ?? new Set()) as Set<unknown>;
  135. fullTextAttrs.add(value);
  136. this.setMetadata(key, fullTextAttrs);
  137. return this;
  138. },
  139. setMetadata(key: string, value: unknown) {
  140. resourceState.shared.set(key, value);
  141. return this;
  142. },
  143. name<NewName extends ResourceType['name']>(n: NewName) {
  144. resourceState.itemName = n;
  145. return this;
  146. },
  147. route<NewRouteName extends ResourceType['routeName']>(n: NewRouteName) {
  148. resourceState.routeName = n;
  149. return this;
  150. },
  151. get itemName() {
  152. return resourceState.itemName;
  153. },
  154. get routeName() {
  155. return resourceState.routeName;
  156. },
  157. get schema() {
  158. return schema;
  159. },
  160. relatesTo<RelatedSchema extends v.BaseSchema>(
  161. objectResource: Resource<ResourceType & { schema: RelatedSchema }>,
  162. relationshipParams: Relationship<ResourceType['schema'], RelatedSchema>,
  163. ) {
  164. resourceState.relationships.set(relationshipParams.name, {
  165. ...relationshipParams,
  166. objectResource,
  167. });
  168. return this;
  169. },
  170. createdAt<NewCreatedAtAttr extends ResourceType['createdAtAttr']>(n: NewCreatedAtAttr) {
  171. resourceState.shared.set('createdAtAttr', n);
  172. return this;
  173. },
  174. updatedAt<NewUpdatedAtAttr extends ResourceType['updatedAtAttr']>(n: NewUpdatedAtAttr) {
  175. resourceState.shared.set('updatedAtAttr', n);
  176. return this;
  177. },
  178. } as Resource<ResourceType>;
  179. };
  180. export type ResourceType<R extends Resource> = v.Output<R['schema']>;
  181. export const getAcceptPatchString = (canPatch: CanPatchObject) => {
  182. const validPatchTypes = Object.entries(canPatch)
  183. .filter(([, allowed]) => allowed)
  184. .map(([patchType]) => patchType);
  185. return Object.entries(PATCH_CONTENT_MAP_TYPE)
  186. .filter(([, patchType]) => validPatchTypes.includes(patchType))
  187. .map(([contentType ]) => contentType)
  188. .join(',');
  189. }