@occultist/occultist 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/dist/accept.d.ts +41 -0
  4. package/dist/accept.js +110 -0
  5. package/dist/accept.test.d.ts +1 -0
  6. package/dist/accept.test.js +44 -0
  7. package/dist/action.test.d.ts +1 -0
  8. package/dist/action.test.js +1 -0
  9. package/dist/actions/actionSets.d.ts +23 -0
  10. package/dist/actions/actionSets.js +49 -0
  11. package/dist/actions/actions.d.ts +163 -0
  12. package/dist/actions/actions.js +436 -0
  13. package/dist/actions/context.d.ts +78 -0
  14. package/dist/actions/context.js +112 -0
  15. package/dist/actions/meta.d.ts +49 -0
  16. package/dist/actions/meta.js +177 -0
  17. package/dist/actions/path.d.ts +21 -0
  18. package/dist/actions/path.js +83 -0
  19. package/dist/actions/path.test.d.ts +1 -0
  20. package/dist/actions/path.test.js +9 -0
  21. package/dist/actions/spec.d.ts +214 -0
  22. package/dist/actions/spec.js +1 -0
  23. package/dist/actions/types.d.ts +112 -0
  24. package/dist/actions/types.js +2 -0
  25. package/dist/actions/writer.d.ts +27 -0
  26. package/dist/actions/writer.js +140 -0
  27. package/dist/actions/writer.test.d.ts +1 -0
  28. package/dist/actions/writer.test.js +42 -0
  29. package/dist/auth/types.d.ts +14 -0
  30. package/dist/auth/types.js +1 -0
  31. package/dist/cache/cache.d.ts +30 -0
  32. package/dist/cache/cache.js +220 -0
  33. package/dist/cache/etag.d.ts +17 -0
  34. package/dist/cache/etag.js +83 -0
  35. package/dist/cache/etag.test.d.ts +1 -0
  36. package/dist/cache/etag.test.js +91 -0
  37. package/dist/cache/memory.d.ts +12 -0
  38. package/dist/cache/memory.js +36 -0
  39. package/dist/cache/types.d.ts +175 -0
  40. package/dist/cache/types.js +4 -0
  41. package/dist/errors.d.ts +11 -0
  42. package/dist/errors.js +54 -0
  43. package/dist/jsonld.d.ts +43 -0
  44. package/dist/jsonld.js +1 -0
  45. package/dist/makeTypeDefs.d.ts +27 -0
  46. package/dist/makeTypeDefs.js +70 -0
  47. package/dist/merge.d.ts +61 -0
  48. package/dist/merge.js +1 -0
  49. package/dist/mod.d.ts +14 -0
  50. package/dist/mod.js +14 -0
  51. package/dist/processAction.d.ts +15 -0
  52. package/dist/processAction.js +512 -0
  53. package/dist/registry.d.ts +88 -0
  54. package/dist/registry.js +314 -0
  55. package/dist/registry.test.d.ts +1 -0
  56. package/dist/registry.test.js +133 -0
  57. package/dist/request.d.ts +29 -0
  58. package/dist/request.js +118 -0
  59. package/dist/scopes.d.ts +35 -0
  60. package/dist/scopes.js +121 -0
  61. package/dist/scopes.test.d.ts +1 -0
  62. package/dist/scopes.test.js +55 -0
  63. package/dist/transformers/fileTransformer.d.ts +1 -0
  64. package/dist/transformers/fileTransformer.js +8 -0
  65. package/dist/types.d.ts +12 -0
  66. package/dist/types.js +1 -0
  67. package/dist/utils/alwaysArray.d.ts +1 -0
  68. package/dist/utils/alwaysArray.js +9 -0
  69. package/dist/utils/contextBuilder.d.ts +9 -0
  70. package/dist/utils/contextBuilder.js +82 -0
  71. package/dist/utils/getActionContext.d.ts +7 -0
  72. package/dist/utils/getActionContext.js +48 -0
  73. package/dist/utils/getInternalName.d.ts +6 -0
  74. package/dist/utils/getInternalName.js +7 -0
  75. package/dist/utils/getParamLocation.d.ts +2 -0
  76. package/dist/utils/getParamLocation.js +6 -0
  77. package/dist/utils/getPropertyValueSpecifications.d.ts +2 -0
  78. package/dist/utils/getPropertyValueSpecifications.js +49 -0
  79. package/dist/utils/getRequestBodyValues.d.ts +11 -0
  80. package/dist/utils/getRequestBodyValues.js +122 -0
  81. package/dist/utils/getRequestIRIValues.d.ts +14 -0
  82. package/dist/utils/getRequestIRIValues.js +133 -0
  83. package/dist/utils/isBodyInit.d.ts +1 -0
  84. package/dist/utils/isBodyInit.js +21 -0
  85. package/dist/utils/isNil.d.ts +1 -0
  86. package/dist/utils/isNil.js +4 -0
  87. package/dist/utils/isObject.d.ts +6 -0
  88. package/dist/utils/isObject.js +6 -0
  89. package/dist/utils/isPopulatedObject.d.ts +5 -0
  90. package/dist/utils/isPopulatedObject.js +8 -0
  91. package/dist/utils/isPopulatedString.d.ts +1 -0
  92. package/dist/utils/isPopulatedString.js +4 -0
  93. package/dist/utils/joinPaths.d.ts +1 -0
  94. package/dist/utils/joinPaths.js +31 -0
  95. package/dist/utils/makeAppendProblemDetails.d.ts +14 -0
  96. package/dist/utils/makeAppendProblemDetails.js +26 -0
  97. package/dist/utils/makeURLPattern.d.ts +5 -0
  98. package/dist/utils/makeURLPattern.js +12 -0
  99. package/dist/utils/normalizeURL.d.ts +4 -0
  100. package/dist/utils/normalizeURL.js +11 -0
  101. package/dist/utils/parseSearchParams.d.ts +3 -0
  102. package/dist/utils/parseSearchParams.js +24 -0
  103. package/dist/utils/preferredMediaTypes.d.ts +42 -0
  104. package/dist/utils/preferredMediaTypes.js +149 -0
  105. package/dist/utils/urlToIRI.d.ts +1 -0
  106. package/dist/utils/urlToIRI.js +8 -0
  107. package/dist/utils/validateSpecValue.d.ts +1 -0
  108. package/dist/utils/validateSpecValue.js +1 -0
  109. package/dist/validators.d.ts +16 -0
  110. package/dist/validators.js +134 -0
  111. package/lib/accept.test.ts +55 -0
  112. package/lib/accept.ts +147 -0
  113. package/lib/action.test.ts +2 -0
  114. package/lib/actions/actionSets.ts +88 -0
  115. package/lib/actions/actions.ts +795 -0
  116. package/lib/actions/context.ts +170 -0
  117. package/lib/actions/meta.ts +251 -0
  118. package/lib/actions/path.test.ts +15 -0
  119. package/lib/actions/path.ts +99 -0
  120. package/lib/actions/spec.ts +545 -0
  121. package/lib/actions/types.ts +146 -0
  122. package/lib/actions/writer.test.ts +57 -0
  123. package/lib/actions/writer.ts +176 -0
  124. package/lib/auth/types.ts +22 -0
  125. package/lib/cache/cache.ts +291 -0
  126. package/lib/cache/etag.test.ts +122 -0
  127. package/lib/cache/etag.ts +106 -0
  128. package/lib/cache/memory.ts +52 -0
  129. package/lib/cache/types.ts +240 -0
  130. package/lib/errors.ts +66 -0
  131. package/lib/jsonld.ts +67 -0
  132. package/lib/makeTypeDefs.ts +138 -0
  133. package/lib/merge.ts +86 -0
  134. package/lib/mod.ts +14 -0
  135. package/lib/processAction.ts +690 -0
  136. package/lib/registry.test.ts +174 -0
  137. package/lib/registry.ts +455 -0
  138. package/lib/request.ts +153 -0
  139. package/lib/scopes.test.ts +70 -0
  140. package/lib/scopes.ts +178 -0
  141. package/lib/transformers/fileTransformer.ts +10 -0
  142. package/lib/types.ts +13 -0
  143. package/lib/utils/alwaysArray.ts +10 -0
  144. package/lib/utils/contextBuilder.ts +111 -0
  145. package/lib/utils/getActionContext.ts +76 -0
  146. package/lib/utils/getInternalName.ts +15 -0
  147. package/lib/utils/getParamLocation.ts +14 -0
  148. package/lib/utils/getPropertyValueSpecifications.ts +76 -0
  149. package/lib/utils/getRequestBodyValues.ts +155 -0
  150. package/lib/utils/getRequestIRIValues.ts +201 -0
  151. package/lib/utils/isBodyInit.ts +22 -0
  152. package/lib/utils/isNil.ts +4 -0
  153. package/lib/utils/isObject.ts +8 -0
  154. package/lib/utils/isPopulatedObject.ts +9 -0
  155. package/lib/utils/isPopulatedString.ts +4 -0
  156. package/lib/utils/joinPaths.ts +36 -0
  157. package/lib/utils/makeAppendProblemDetails.ts +57 -0
  158. package/lib/utils/makeURLPattern.ts +18 -0
  159. package/lib/utils/normalizeURL.ts +15 -0
  160. package/lib/utils/parseSearchParams.ts +36 -0
  161. package/lib/utils/preferredMediaTypes.ts +220 -0
  162. package/lib/utils/urlToIRI.ts +11 -0
  163. package/lib/utils/validateSpecValue.ts +0 -0
  164. package/lib/validators.ts +186 -0
  165. package/package.json +41 -0
@@ -0,0 +1,76 @@
1
+ import type { OrArray, JSONValue } from "../jsonld.js";
2
+ import type { ActionSpec, PropertySpec, ResponseInputSpec, SpecValue, ActionOption } from "../actions/spec.js";
3
+ import { isNil } from "./isNil.js";
4
+ import { isObject } from "./isObject.js";
5
+
6
+
7
+ // deno-lint-ignore no-explicit-any
8
+ export async function getPropertyValueSpecifications(spec: ActionSpec<any>) {
9
+ async function splitSpecAndValue(
10
+ propertySpec: PropertySpec,
11
+ ): Promise<
12
+ [ResponseInputSpec, SpecValue | OrArray<ActionOption<JSONValue>> | null]
13
+ > {
14
+ let options: OrArray<ActionOption<JSONValue>> | null = null;
15
+
16
+ const specValue: SpecValue = {};
17
+ const inputSpec: ResponseInputSpec = {
18
+ '@type': 'https://schema.org/PropertyValueSpecification',
19
+ defaultValue: propertySpec.defaultValue,
20
+ maxValue: propertySpec.maxValue,
21
+ minValue: propertySpec.minValue,
22
+ readonlyValue: propertySpec.readonlyValue,
23
+ stepValue: propertySpec.stepValue,
24
+ valueName: propertySpec.valueName,
25
+ valueRequired: propertySpec.valueRequired,
26
+ multipleValues: propertySpec.multipleValues,
27
+ valueMinLength: propertySpec.valueMinLength,
28
+ valueMaxLength: propertySpec.valueMaxLength,
29
+ valuePatern: propertySpec.valuePattern,
30
+ };
31
+
32
+ if (typeof propertySpec.options === 'function') {
33
+ options = await propertySpec.options();
34
+ } else if (!isNil(propertySpec.options)) {
35
+ options = propertySpec.options;
36
+ }
37
+
38
+ if (!isObject(propertySpec.properties)) {
39
+ return [inputSpec, options];
40
+ }
41
+
42
+ for (
43
+ const [term, childPropertySpec] of Object.entries(
44
+ propertySpec.properties,
45
+ )
46
+ ) {
47
+ const [childInputSpec, childSpecValue] = await splitSpecAndValue(
48
+ childPropertySpec,
49
+ );
50
+
51
+ specValue[`${term}-input`] = childInputSpec;
52
+
53
+ if (childSpecValue != null) {
54
+ specValue[term] = childSpecValue;
55
+ }
56
+ }
57
+
58
+ return [inputSpec, specValue];
59
+ }
60
+
61
+ const specValue: SpecValue = {};
62
+
63
+ for (const [term, propertySpec] of Object.entries(spec)) {
64
+ const [childInputSpec, childSpecValue] = await splitSpecAndValue(
65
+ propertySpec,
66
+ );
67
+
68
+ specValue[`${term}-input`] = childInputSpec;
69
+
70
+ if (childSpecValue != null) {
71
+ specValue[term] = childSpecValue;
72
+ }
73
+ }
74
+
75
+ return specValue;
76
+ }
@@ -0,0 +1,155 @@
1
+ import { ProblemDetailsError } from "../errors.js";
2
+ import type { JSONValue, ContextDefinition, JSONObject } from "../jsonld.js";
3
+ import type { ContextState, ActionSpec, PropertySpec } from "../actions/spec.js";
4
+ import jsonld from 'jsonld';
5
+ import type { ImplementedAction } from "../actions/types.js";
6
+
7
+
8
+ // export type BodyValue = Record<string, FileInput | FileInput[] | JSONValue>;
9
+ export type BodyValue = Record<string, JSONValue>;
10
+
11
+
12
+ export type RequestBodyResult = {
13
+ bodyValues: BodyValue;
14
+ };
15
+
16
+ export async function getRequestBodyValues<
17
+ State extends ContextState = ContextState,
18
+ Spec extends ActionSpec<ContextState> = ActionSpec<ContextState>,
19
+ >({
20
+ req,
21
+ action,
22
+ }: {
23
+ req: Request,
24
+ action: ImplementedAction<State, Spec>,
25
+ }): Promise<RequestBodyResult> {
26
+ let bodyValues: BodyValue = {};
27
+ const contentType = req.headers.get('content-type');
28
+ const mappedTypes: Record<string, {
29
+ term: string;
30
+ propertySpec: PropertySpec;
31
+ }> = Object.entries<PropertySpec>(action.spec)
32
+ .reduce((acc, [term, propertySpec]) => {
33
+ return {
34
+ ...acc,
35
+ [propertySpec.typeDef.term]: {
36
+ term,
37
+ propertySpec,
38
+ },
39
+ [propertySpec.type || propertySpec.typeDef?.type]: {
40
+ term,
41
+ propertySpec,
42
+ },
43
+ };
44
+ }, {});
45
+
46
+ if (contentType?.startsWith('multipart/form-data')) {
47
+ // multipart should be sent using expanded types which get normalized
48
+ // into the compact terms for each type.
49
+ // otherwise json requests would need to also need to use
50
+ // expanded terms.
51
+ const formData = await req.formData();
52
+
53
+ for (const [name, part] of formData.entries()) {
54
+ if (typeof name !== 'string') {
55
+ throw new ProblemDetailsError(400, {
56
+ title: 'Unnamed parameter in request multipart body',
57
+ });
58
+ }
59
+
60
+ let term: string | undefined;
61
+ let propertySpec: PropertySpec | undefined;
62
+
63
+ if (typeof part !== 'string' && (part.type && !part.type.startsWith('text/plain'))) {
64
+ term = mappedTypes[name].term;
65
+ propertySpec = mappedTypes[name].propertySpec;
66
+
67
+ if (!term || !propertySpec) {
68
+ continue;
69
+ }
70
+
71
+ if (propertySpec.dataType !== 'file') {
72
+ throw new ProblemDetailsError(400, {
73
+ title: `Unexpected content '${name}' in request multipart body`,
74
+ });
75
+ }
76
+
77
+ //bodyValues[term] = part;
78
+ bodyValues[term] = null;
79
+ continue;
80
+ }
81
+
82
+ if (typeof part === 'string' || part.type === 'text/plain' || part.type == null) {
83
+ term = mappedTypes[name].term;
84
+ propertySpec = mappedTypes[name].propertySpec;
85
+ } else if (part.name) {
86
+ term = mappedTypes[name].term;
87
+ propertySpec = mappedTypes[name].propertySpec;
88
+ } else {
89
+ throw new ProblemDetailsError(400, {
90
+ title: `Unexpected content '${name}' in request multipart body`,
91
+ });
92
+ }
93
+
94
+ if (!term || !propertySpec) {
95
+ continue;
96
+ }
97
+
98
+ const textValue = typeof part === 'string' ? part : await part.text();
99
+
100
+ if (!textValue) {
101
+ continue;
102
+ }
103
+
104
+ if (propertySpec.dataType === 'number' && /\d+(\.\d+)?/.test(textValue)) {
105
+ bodyValues[term] = Number(textValue);
106
+ } else if (propertySpec.dataType === 'boolean') {
107
+ bodyValues[term] = textValue === 'true';
108
+ } else {
109
+ bodyValues[term] = textValue;
110
+ }
111
+ }
112
+ } else if (contentType?.startsWith('application/json')) {
113
+ try {
114
+ bodyValues = await req.json();
115
+ } catch {
116
+ throw new ProblemDetailsError(400, {
117
+ title: 'Failed to parse JSON body',
118
+ });
119
+ }
120
+ } else if (contentType?.startsWith('application/ld+json')) {
121
+ let source: JSONValue;
122
+ let expanded: jsonld.JsonLdDocument;
123
+ let compacted: jsonld.NodeObject | undefined;
124
+
125
+ try {
126
+ source = await req.json();
127
+ } catch {
128
+ throw new ProblemDetailsError(400, {
129
+ title: 'Failed to parse JSON body',
130
+ });
131
+ }
132
+
133
+ try {
134
+ expanded = await jsonld.expand(source as jsonld.JsonLdDocument);
135
+ } catch {
136
+ throw new ProblemDetailsError(400, {
137
+ title: 'Failed to expand JSON-LD body',
138
+ });
139
+ }
140
+
141
+ try {
142
+ compacted = await jsonld.compact(expanded, action.context as ContextDefinition)
143
+ } catch {
144
+ throw new ProblemDetailsError(400, {
145
+ title: 'Failed to compact JSON-LD body',
146
+ });
147
+ }
148
+
149
+ delete compacted['@context'];
150
+
151
+ bodyValues = compacted as JSONObject;
152
+ }
153
+
154
+ return { bodyValues };
155
+ }
@@ -0,0 +1,201 @@
1
+ import type { ImplementedAction } from "../actions/types.js";
2
+ import { ProblemDetailsError } from "../errors.js";
3
+ import type { ActionSpec, ContextState, FileSingleSpec, FileMultiSpec, BooleanSingleSpec, BooleanMultiSpec, NumberSingleSpec, NumberMultiSpec, StringSingleSpec, StringMultiSpec, ParsedIRIValues, PropertySpec } from "../actions/spec.js";
4
+ import { getParamLocation } from "./getParamLocation.js";
5
+
6
+
7
+ export type IRIValue<
8
+ Spec extends ActionSpec<ContextState> = ActionSpec<ContextState>,
9
+ > = {
10
+ [Term in keyof Spec]: (
11
+ Spec[Term] extends FileSingleSpec | FileMultiSpec ? never
12
+ : Spec[Term]['valueName'] extends string ? (
13
+ Spec[Term] extends BooleanSingleSpec ? boolean
14
+ : Spec[Term] extends BooleanMultiSpec ? boolean[]
15
+ : Spec[Term] extends NumberSingleSpec ? number
16
+ : Spec[Term] extends NumberMultiSpec ? number[]
17
+ : Spec[Term] extends StringSingleSpec ? string
18
+ : Spec[Term] extends StringMultiSpec ? string[]
19
+ : never
20
+ )
21
+ : never
22
+ );
23
+ };
24
+
25
+ export type RequestIRIResult<
26
+ Spec extends ActionSpec<ContextState> = ActionSpec<ContextState>,
27
+ > = {
28
+ pathValues: ParsedIRIValues;
29
+ queryValues: ParsedIRIValues;
30
+ iriValues: IRIValue<Spec>;
31
+ };
32
+
33
+ export function getRequestIRIValues<
34
+ State extends ContextState = ContextState,
35
+ Spec extends ActionSpec<ContextState> = ActionSpec<ContextState>,
36
+ >({
37
+ iri,
38
+ action,
39
+ }: {
40
+ iri: string;
41
+ action: ImplementedAction<State, Spec>;
42
+ }): RequestIRIResult<Spec> {
43
+ const pathValues: ParsedIRIValues = {};
44
+ const queryValues: ParsedIRIValues = {};
45
+ // deno-lint-ignore no-explicit-any
46
+ const iriValues = {} as IRIValue<any>;
47
+ const urlPatternResult = action.pattern.exec(iri);
48
+ const pathParams = urlPatternResult?.pathname.groups || {};
49
+ const searchParams = new URL(iri).searchParams;
50
+
51
+ const valueNames = Object.values<PropertySpec>(action.spec)
52
+ .filter((specItem) => typeof specItem.valueName === 'string')
53
+ .map((specItem) => specItem.valueName);
54
+
55
+ //if (action.strict) {
56
+ for (const valueName of searchParams.keys()) {
57
+ if (!valueNames.includes(valueName)) {
58
+ throw new ProblemDetailsError(400, {
59
+ title: `Unexpected value "${valueName}"`,
60
+ });
61
+ }
62
+ }
63
+ //}
64
+
65
+ for (
66
+ const [term, specItem] of Object.entries(
67
+ action.spec as unknown as Record<string, PropertySpec>,
68
+ )
69
+ ) {
70
+ if (typeof specItem.valueName !== 'string') {
71
+ continue;
72
+ }
73
+
74
+ let value: string | string[] | undefined | null;
75
+ const valueName = specItem.valueName;
76
+ const multipleValues = Boolean(specItem.multipleValues);
77
+ const paramLocation = getParamLocation(valueName, action.pattern);
78
+
79
+ if (paramLocation === 'path') {
80
+ value = pathParams[valueName];
81
+ } else {
82
+ value = searchParams.getAll(valueName);
83
+ }
84
+
85
+ if (value === null) {
86
+ value = undefined;
87
+ }
88
+
89
+ if (!multipleValues && Array.isArray(value) && value.length > 1) {
90
+ throw new ProblemDetailsError(400, {
91
+ title: `Invalid request`,
92
+ errors: [{
93
+ name: term,
94
+ pointer: `#${term}`,
95
+ reason: `Received array when expecting single value`,
96
+ }],
97
+ });
98
+ }
99
+
100
+ if (multipleValues && typeof value === 'string') {
101
+ value = [value];
102
+ } else if (!multipleValues && Array.isArray(value)) {
103
+ value = value[0];
104
+ }
105
+
106
+ if (typeof value === 'undefined') {
107
+ continue;
108
+ } else if (!specItem.valueRequired && Array.isArray(value) && value.length === 0) {
109
+ continue;
110
+ }
111
+
112
+ if (specItem.dataType === 'boolean' && typeof value === 'string') {
113
+ iriValues[term] = parseBoolean({
114
+ term,
115
+ value,
116
+ pointer: `#${term}`,
117
+ });
118
+ } else if (specItem.dataType === 'boolean' && Array.isArray(value)) {
119
+ iriValues[term] = value.map((value, index) => parseBoolean({
120
+ term,
121
+ value,
122
+ pointer: `#${term}[${index}]`,
123
+ }));
124
+ } else if (specItem.dataType === 'number' && typeof value === 'string') {
125
+ iriValues[term] = parseNumber({
126
+ term,
127
+ value,
128
+ pointer: `#${term}`,
129
+ });
130
+ } else if (specItem.dataType === 'number' && Array.isArray(value)) {
131
+ iriValues[term] = value.map((value, index) => parseNumber({
132
+ term,
133
+ value,
134
+ pointer: `#${term}[${index}]`,
135
+ }));
136
+ } else {
137
+ // string values don't require parsing
138
+ iriValues[term] = value;
139
+ }
140
+
141
+ if (paramLocation === 'path') {
142
+ pathValues[term] = iriValues[term];
143
+ } else {
144
+ queryValues[term] = iriValues[term];
145
+ }
146
+ }
147
+
148
+ return {
149
+ pathValues,
150
+ queryValues,
151
+ iriValues,
152
+ };
153
+ }
154
+
155
+ function parseBoolean({
156
+ term,
157
+ value,
158
+ pointer,
159
+ }: {
160
+ term: string;
161
+ value: string;
162
+ pointer: string;
163
+ }): boolean {
164
+ if (!['', 'true', 'false'].includes(value)) {
165
+ throw new ProblemDetailsError(400, {
166
+ title: `Invalid request`,
167
+ errors: [{
168
+ name: term,
169
+ pointer,
170
+ reason: `Boolean values must be "", "true" or "false"`,
171
+ }],
172
+ })
173
+ }
174
+
175
+ return value === '' || value === 'true';
176
+ }
177
+
178
+ function parseNumber({
179
+ term,
180
+ value,
181
+ pointer,
182
+ }: {
183
+ term: string;
184
+ value: string;
185
+ pointer: string;
186
+ }): number {
187
+ const numberValue = Number(value);
188
+
189
+ if (isNaN(numberValue) || !Number.isFinite(numberValue)) {
190
+ throw new ProblemDetailsError(400, {
191
+ title: `Invalid request`,
192
+ errors: [{
193
+ name: term,
194
+ pointer,
195
+ reason: `Numeric values must valid numbers and finite`,
196
+ }],
197
+ });
198
+ }
199
+
200
+ return numberValue;
201
+ }
@@ -0,0 +1,22 @@
1
+
2
+ export function isBodyInit(value: unknown): value is BodyInit {
3
+ return value instanceof ReadableStream ||
4
+ value instanceof Blob ||
5
+ value instanceof DataView ||
6
+ value instanceof ArrayBuffer ||
7
+ value instanceof Int8Array ||
8
+ value instanceof Uint8Array ||
9
+ value instanceof Uint8ClampedArray ||
10
+ value instanceof Int16Array ||
11
+ value instanceof Uint16Array ||
12
+ value instanceof Int32Array ||
13
+ value instanceof Uint32Array ||
14
+ value instanceof Float16Array ||
15
+ value instanceof Float32Array ||
16
+ value instanceof Float64Array ||
17
+ value instanceof BigInt64Array ||
18
+ value instanceof BigUint64Array ||
19
+ value instanceof FormData ||
20
+ value instanceof URLSearchParams ||
21
+ typeof value === 'string';
22
+ }
@@ -0,0 +1,4 @@
1
+ // deno-lint-ignore no-explicit-any
2
+ export function isNil(value: any): value is null | undefined {
3
+ return typeof value === 'undefined' || value === null;
4
+ }
@@ -0,0 +1,8 @@
1
+ type ObjectValue = Record<string | number | symbol, unknown>;
2
+
3
+ /**
4
+ * Returns true if the input value is a plain Javascript object.
5
+ */
6
+ export function isObject<T extends ObjectValue = ObjectValue>(value: unknown): value is T {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
8
+ }
@@ -0,0 +1,9 @@
1
+ import { isObject } from './isObject.js';
2
+
3
+ /**
4
+ * Returns true if the input value is a plain Javascript object with
5
+ * at least one member.
6
+ */
7
+ export function isPopulatedObject<ChildType>(value: unknown): value is Record<string, ChildType> {
8
+ return isObject(value) && Object.keys(value).length > 0;
9
+ }
@@ -0,0 +1,4 @@
1
+ // deno-lint-ignore no-explicit-any
2
+ export function isPopulatedString(value: any): value is string {
3
+ return typeof value === 'string' && value.length !== 0;
4
+ }
@@ -0,0 +1,36 @@
1
+ export function joinPaths(...paths: Array<string | number>): string {
2
+ let fullPath: string = '';
3
+
4
+ if (paths[0] == null || paths[0] == '') {
5
+ throw new Error(
6
+ 'The first path must be a populated string when joining paths',
7
+ );
8
+ }
9
+
10
+ for (const pathSrc of paths) {
11
+ let path: string | null = null;
12
+
13
+ if (typeof pathSrc === 'number') {
14
+ path = `${pathSrc}`;
15
+ } else if (typeof pathSrc === 'string') {
16
+ path = pathSrc;
17
+ }
18
+
19
+ if (path == null || path == '') {
20
+ continue;
21
+ }
22
+
23
+ if (fullPath === '') {
24
+ fullPath = path;
25
+ } else if (fullPath.endsWith('/') && path.startsWith('/')) {
26
+ fullPath += path.replace(/^\//, '');
27
+ } else if (fullPath.endsWith('/') || path.startsWith('/')) {
28
+ fullPath += path;
29
+ } else {
30
+ fullPath += `/${path}`;
31
+ }
32
+ }
33
+
34
+ return fullPath;
35
+ }
36
+
@@ -0,0 +1,57 @@
1
+ import type {ProblemDetailsParam, ProblemDetails} from '../types.js';
2
+
3
+ export type AppendProblemDetails = (args: {
4
+ status: number;
5
+ title?: string,
6
+ detail?: string,
7
+ param?: ProblemDetailsParam;
8
+ }) => void;
9
+
10
+ export type ProblemDetailsParamsRefs = {
11
+ title?: string;
12
+ detail?: string;
13
+ httpStatus?: number;
14
+ problemDetails?: ProblemDetails;
15
+ };
16
+
17
+ export function makeAppendProblemDetails(
18
+ refs: ProblemDetailsParamsRefs
19
+ ): AppendProblemDetails {
20
+ function appendProblemDetails({
21
+ title,
22
+ detail,
23
+ status,
24
+ param,
25
+ }: {
26
+ status: number;
27
+ title?: string,
28
+ detail?: string,
29
+ param?: ProblemDetailsParam;
30
+ }) {
31
+ if (refs.httpStatus == null) {
32
+ refs.httpStatus = status;
33
+ } else if (
34
+ refs.httpStatus !== status &&
35
+ refs.httpStatus < 400
36
+ ) {
37
+ refs.httpStatus = status
38
+ }
39
+
40
+ if (!refs.problemDetails) {
41
+ refs.problemDetails = {
42
+ title: title ??= "Bad request",
43
+ detail: detail ??= "Invalid parameters",
44
+ };
45
+ }
46
+
47
+ if (param) {
48
+ if (!Array.isArray(refs.problemDetails.errors)) {
49
+ refs.problemDetails.errors = [param];
50
+ } else {
51
+ refs.problemDetails.errors.push(param);
52
+ }
53
+ }
54
+ }
55
+
56
+ return appendProblemDetails;
57
+ }
@@ -0,0 +1,18 @@
1
+ import url from 'node:url';
2
+
3
+
4
+ type URLPatternConstructor = new (input: string, baseURL: string) => URLPattern;
5
+
6
+ /**
7
+ * Work-around for how URLPatterns are currently supported across runtimes.
8
+ * This requires node v23 and above to run.
9
+ */
10
+ export function makeURLPattern(pattern: string, baseURL: string): URLPattern {
11
+ if (typeof URLPattern === 'undefined') {
12
+ const URLPattern = (url as unknown as { URLPattern: URLPatternConstructor }).URLPattern;
13
+
14
+ return new URLPattern(pattern, baseURL);
15
+ }
16
+
17
+ return new URLPattern(pattern, baseURL);
18
+ }
@@ -0,0 +1,15 @@
1
+
2
+
3
+ /**
4
+ * Maps the URL of the request to the root IRI.
5
+ */
6
+ export function normalizeURL(rootIRI: string, urlStr: string): string {
7
+ const sourceURL = new URL(urlStr);
8
+ const targetURL = new URL(rootIRI);
9
+
10
+ targetURL.pathname = sourceURL.pathname;
11
+ targetURL.search = sourceURL.search;
12
+ targetURL.hash = sourceURL.hash;
13
+
14
+ return targetURL.toString();
15
+ }
@@ -0,0 +1,36 @@
1
+ import type {ActionSpec, ContextState} from "../actions/spec.js";
2
+ import type { EmptyObject, JSONObject, JSONValue } from "../jsonld.js";
3
+
4
+ export function parseSearchParams<
5
+ ActionState extends ContextState = EmptyObject,
6
+ >(
7
+ spec: ActionSpec<ActionState>,
8
+ searchParams: URLSearchParams,
9
+ ): JSONObject {
10
+ const body: Record<string, JSONValue> = {};
11
+
12
+ for (const [key, specItem] of Object.entries(spec)) {
13
+ const value = searchParams.get(key);
14
+
15
+ if (
16
+ specItem.dataType === "number" &&
17
+ Array.isArray(value) &&
18
+ value !== null
19
+ ) {
20
+ body[key] = value.map(Number);
21
+ } else if (
22
+ specItem.dataType === "number" &&
23
+ value !== null &&
24
+ isNaN(Number(value))
25
+ ) {
26
+ // keeping the number value as a string allows error handling to work with it
27
+ body[key] = value;
28
+ } else if (specItem.dataType === "number" && value !== null) {
29
+ body[key] = Number(value);
30
+ } else if (value !== null) {
31
+ body[key] = value;
32
+ }
33
+ }
34
+
35
+ return body;
36
+ }