@svadmin/core 0.54.0 → 0.55.0
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.
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/resource-contract.ts +111 -2
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -67,7 +67,7 @@ export {
|
|
|
67
67
|
useCreate, useUpdate, useDelete, useCreateMany, useUpdateMany, useDeleteMany, useForm,
|
|
68
68
|
useInvalidate,
|
|
69
69
|
} from './strict-hooks.svelte';
|
|
70
|
-
export { defineResource, getContractFormFields } from './resource-contract';
|
|
70
|
+
export { defineResource, getContractFormFields, withContractProjection } from './resource-contract';
|
|
71
71
|
export { UpdateManyPartialError } from './update-many-contract';
|
|
72
72
|
export { CreateManyPartialError } from './create-many-contract';
|
|
73
73
|
export type { ContractCreateOptions as UseCreateOptions } from './create-hooks.svelte';
|
|
@@ -77,6 +77,7 @@ export type { ContractUpdateParams as UseUpdateMutateParams } from './update-con
|
|
|
77
77
|
export type { ContractDeleteOptions as UseDeleteOptions } from './delete-hooks.svelte';
|
|
78
78
|
export type { ContractDeleteParams as UseDeleteMutateParams } from './delete-contract';
|
|
79
79
|
export type { ContractFormAction, ContractFormDraft, ContractFormValues } from './resource-contract';
|
|
80
|
+
export type { ContractProjectionOptions } from './resource-contract';
|
|
80
81
|
export type { ResourceContract, ContractSchemas, ContractRecord, ContractId, ContractInput } from './resource-contract';
|
|
81
82
|
export type { ContractFilter, ContractSort } from './strict-hooks.svelte';
|
|
82
83
|
export { defineCommand } from './command-contract';
|
package/src/resource-contract.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { CloneType, Kind, TransformKind, Type, type Static, type TObject, type T
|
|
|
2
2
|
import { checkExact, createExactSchemaValidator } from './schema-validation';
|
|
3
3
|
import { TypeGuard } from '@sinclair/typebox/type';
|
|
4
4
|
import { DeleteManyPartialError, HttpError, type DataProvider, type KnownResources, type BaseRecord,
|
|
5
|
-
type GetManyParams, type CreateManyParams, type UpdateManyParams, type DeleteManyParams,
|
|
6
|
-
type GetOneResult, type GetManyResult } from './types';
|
|
5
|
+
type GetListResult, type GetManyParams, type CreateManyParams, type UpdateManyParams, type DeleteManyParams,
|
|
6
|
+
type GetOneParams, type GetOneResult, type GetManyResult } from './types';
|
|
7
7
|
import { withResourceSchemas, createResourceRequestValidator, type ResourceSchemas } from './resource-schemas';
|
|
8
8
|
import { decodeBaseRecord } from './record-decoder';
|
|
9
9
|
import { snapshotPlainData } from './plain-data';
|
|
@@ -166,6 +166,115 @@ export function parseContractRecord(contract: ResourceContract, value: unknown):
|
|
|
166
166
|
throw new HttpError('Invalid provider response', 502, undefined, { code: 'INVALID_PROVIDER_RESPONSE' });
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
export interface ContractProjectionOptions {
|
|
170
|
+
/** Supplies the id when the backend record has no declared contract id. */
|
|
171
|
+
readonly getId?: (input: {
|
|
172
|
+
resource: string;
|
|
173
|
+
requestedId: string | number | undefined;
|
|
174
|
+
record: unknown;
|
|
175
|
+
index: number;
|
|
176
|
+
}) => string | number | undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function projectBySchema(schema: TSchema, value: unknown): unknown {
|
|
180
|
+
if (TypeGuard.IsObject(schema)) {
|
|
181
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return value;
|
|
182
|
+
const source = value as Record<string, unknown>;
|
|
183
|
+
const projected: Record<string, unknown> = {};
|
|
184
|
+
for (const [key, property] of Object.entries(schema.properties)) {
|
|
185
|
+
if (Object.hasOwn(source, key)) projected[key] = projectBySchema(property, source[key]);
|
|
186
|
+
}
|
|
187
|
+
return projected;
|
|
188
|
+
}
|
|
189
|
+
if (TypeGuard.IsArray(schema)) {
|
|
190
|
+
return Array.isArray(value) ? value.map(item => projectBySchema(schema.items, item)) : value;
|
|
191
|
+
}
|
|
192
|
+
if (TypeGuard.IsTuple(schema)) {
|
|
193
|
+
return Array.isArray(value)
|
|
194
|
+
? value.map((item, index) => schema.items?.[index] ? projectBySchema(schema.items[index], item) : item)
|
|
195
|
+
: value;
|
|
196
|
+
}
|
|
197
|
+
if (TypeGuard.IsUnion(schema)) {
|
|
198
|
+
const branch = schema.anyOf.find(candidate => checkExact(candidate, value));
|
|
199
|
+
return branch ? projectBySchema(branch, value) : value;
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function projectRecord(
|
|
205
|
+
contract: ResourceContract,
|
|
206
|
+
resource: string,
|
|
207
|
+
record: unknown,
|
|
208
|
+
index: number,
|
|
209
|
+
requestedId: string | number | undefined,
|
|
210
|
+
options: ContractProjectionOptions,
|
|
211
|
+
): unknown {
|
|
212
|
+
const projected = projectBySchema(definitionOf(contract).schemas.record, record);
|
|
213
|
+
if (typeof projected !== 'object' || projected === null || Array.isArray(projected)) return projected;
|
|
214
|
+
const result = projected as Record<string, unknown>;
|
|
215
|
+
if (!Object.hasOwn(result, 'id')) {
|
|
216
|
+
const id = options.getId?.({ resource, requestedId, record, index })
|
|
217
|
+
?? requestedId
|
|
218
|
+
?? (typeof record === 'object' && record !== null && !Array.isArray(record)
|
|
219
|
+
? ['id', '_id', 'uuid', 'key', 'name'].map(key => (record as Record<string, unknown>)[key])
|
|
220
|
+
.find(candidate => typeof candidate === 'string' || typeof candidate === 'number')
|
|
221
|
+
: undefined)
|
|
222
|
+
?? `${resource}:${index}`;
|
|
223
|
+
result['id'] = id;
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Projects read responses into the exact closed shape required by resource contracts.
|
|
230
|
+
* Write responses remain untouched so mutation validation keeps its existing semantics.
|
|
231
|
+
*/
|
|
232
|
+
export function withContractProjection(
|
|
233
|
+
provider: DataProvider,
|
|
234
|
+
contracts: Readonly<Record<string, ResourceContract>>,
|
|
235
|
+
options: ContractProjectionOptions = {},
|
|
236
|
+
): Readonly<DataProvider> {
|
|
237
|
+
function contractFor(resource: string): ResourceContract | undefined {
|
|
238
|
+
return contracts[resource];
|
|
239
|
+
}
|
|
240
|
+
function projectList(resource: string, result: GetListResult): GetListResult {
|
|
241
|
+
const contract = contractFor(resource);
|
|
242
|
+
if (!contract) return result;
|
|
243
|
+
return {
|
|
244
|
+
...result,
|
|
245
|
+
data: result.data.map((record, index) =>
|
|
246
|
+
projectRecord(contract, resource, record, index, undefined, options) as BaseRecord),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function projectMany(resource: string, result: GetManyResult): GetManyResult {
|
|
250
|
+
const contract = contractFor(resource);
|
|
251
|
+
if (!contract) return result;
|
|
252
|
+
return {
|
|
253
|
+
...result,
|
|
254
|
+
data: result.data.map((record, index) =>
|
|
255
|
+
projectRecord(contract, resource, record, index, undefined, options) as BaseRecord),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function projectOne(params: GetOneParams, result: GetOneResult): GetOneResult {
|
|
259
|
+
const contract = contractFor(params.resource);
|
|
260
|
+
return contract
|
|
261
|
+
? { ...result, data: projectRecord(contract, params.resource, result.data, 0, params.id, options) as BaseRecord }
|
|
262
|
+
: result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const adapted: DataProvider = {
|
|
266
|
+
...provider,
|
|
267
|
+
getList: async params => projectList(params.resource, await provider.getList(params)),
|
|
268
|
+
getOne: async params => projectOne(params, await provider.getOne(params)),
|
|
269
|
+
getApiUrl: () => provider.getApiUrl(),
|
|
270
|
+
};
|
|
271
|
+
if (provider.getMany) {
|
|
272
|
+
const getMany = provider.getMany.bind(provider);
|
|
273
|
+
adapted.getMany = async params => projectMany(params.resource, await getMany(params));
|
|
274
|
+
}
|
|
275
|
+
return Object.freeze(adapted);
|
|
276
|
+
}
|
|
277
|
+
|
|
169
278
|
function idSchema(schemas: ContractSchemas): TSchema {
|
|
170
279
|
const schema = schemas.record.properties['id'];
|
|
171
280
|
if (!schema) throw new HttpError('Missing contract ID schema', 400, undefined, { code: 'INVALID_RESOURCE_CONTRACT' });
|