@fjell/core 4.4.63 → 4.4.65
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/README.md +42 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +125 -25
- package/dist/operations/Operations.d.ts +220 -19
- package/dist/operations/contained.d.ts +3 -3
- package/dist/operations/methods.d.ts +78 -8
- package/dist/operations/primary.d.ts +3 -3
- package/dist/operations/wrappers/createAllWrapper.d.ts +3 -3
- package/dist/operations/wrappers/createFindWrapper.d.ts +11 -5
- package/dist/validation/index.d.ts +3 -1
- package/dist/validation/index.js +92 -1
- package/dist/validation/schema.d.ts +23 -0
- package/dist/validation/types.d.ts +31 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -134,10 +134,51 @@ import {
|
|
|
134
134
|
validateLocations,
|
|
135
135
|
validateKey,
|
|
136
136
|
validatePK,
|
|
137
|
-
validateKeys
|
|
137
|
+
validateKeys,
|
|
138
|
+
validateSchema,
|
|
139
|
+
SchemaValidator
|
|
138
140
|
} from '@fjell/core/validation';
|
|
139
141
|
```
|
|
140
142
|
|
|
143
|
+
#### Schema Validation (Zod, Yup, etc.)
|
|
144
|
+
|
|
145
|
+
Universal schema validation that works with any validator matching the `SchemaValidator` interface. Zod is supported out of the box via duck typing:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { validateSchema } from '@fjell/core/validation';
|
|
149
|
+
import { z } from 'zod';
|
|
150
|
+
|
|
151
|
+
// Define a Zod schema
|
|
152
|
+
const userSchema = z.object({
|
|
153
|
+
name: z.string().min(3),
|
|
154
|
+
age: z.number().min(18),
|
|
155
|
+
email: z.string().email().optional()
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Validate data before caching, sending to API, or persisting
|
|
159
|
+
const validatedUser = await validateSchema(userData, userSchema);
|
|
160
|
+
|
|
161
|
+
// Validation throws ValidationError with FieldError[] on failure
|
|
162
|
+
try {
|
|
163
|
+
await validateSchema({ name: 'Al', age: 10 }, userSchema);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error instanceof ValidationError) {
|
|
166
|
+
console.log(error.fieldErrors);
|
|
167
|
+
// [
|
|
168
|
+
// { path: ['name'], message: 'String must contain at least 3 character(s)', code: 'too_small' },
|
|
169
|
+
// { path: ['age'], message: 'Number must be greater than or equal to 18', code: 'too_small' }
|
|
170
|
+
// ]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Schema Validation Features:**
|
|
176
|
+
- **Universal**: Works with Zod, Yup, Joi, or any validator matching the interface
|
|
177
|
+
- **Type-Safe**: Full TypeScript support with type inference
|
|
178
|
+
- **Error Transformation**: Automatically converts validator errors to Fjell `ValidationError`
|
|
179
|
+
- **Async Support**: Handles both sync and async validation
|
|
180
|
+
- **Available Everywhere**: Use in cache layers, APIs, persistence, or any system boundary
|
|
181
|
+
|
|
141
182
|
#### Location Validation
|
|
142
183
|
|
|
143
184
|
Validates that location key arrays match the expected coordinate hierarchy:
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export * from './validation';
|
|
|
15
15
|
export * from './errors';
|
|
16
16
|
export * from './operations';
|
|
17
17
|
export * from './event';
|
|
18
|
-
export type { Operations, OperationParams, AffectedKeys, CreateOptions, UpdateOptions } from './operations/Operations';
|
|
18
|
+
export type { Operations, OperationParams, AffectedKeys, CreateOptions, UpdateOptions, PaginationOptions, AllOptions, FindOptions, PaginationMetadata, OperationResult, AllOperationResult, FindOperationResult } from './operations/Operations';
|
|
19
19
|
export { isPriKey as isOperationPriKey, isComKey as isOperationComKey } from './operations/Operations';
|
|
20
20
|
export type { GetMethod, CreateMethod, UpdateMethod, RemoveMethod, UpsertMethod, AllMethod, OneMethod, FindMethod, FindOneMethod, FinderMethod, ActionMethod, ActionOperationMethod, AllActionMethod, AllActionOperationMethod, FacetMethod, FacetOperationMethod, AllFacetMethod, AllFacetOperationMethod, OperationsExtensions } from './operations/methods';
|
|
21
21
|
export * from './operations/wrappers';
|
package/dist/index.js
CHANGED
|
@@ -1438,6 +1438,47 @@ var DuplicateError = class extends ActionError {
|
|
|
1438
1438
|
}
|
|
1439
1439
|
};
|
|
1440
1440
|
|
|
1441
|
+
// src/validation/schema.ts
|
|
1442
|
+
async function validateSchema(data, schema) {
|
|
1443
|
+
if (!schema) {
|
|
1444
|
+
return data;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
if (schema.parseAsync) {
|
|
1448
|
+
return await schema.parseAsync(data);
|
|
1449
|
+
}
|
|
1450
|
+
const result = schema.safeParse(data);
|
|
1451
|
+
if (result.success) {
|
|
1452
|
+
return result.data;
|
|
1453
|
+
} else {
|
|
1454
|
+
throw result.error;
|
|
1455
|
+
}
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
if (error && Array.isArray(error.issues)) {
|
|
1458
|
+
const fieldErrors = error.issues.map((issue) => ({
|
|
1459
|
+
path: issue.path,
|
|
1460
|
+
message: issue.message,
|
|
1461
|
+
code: issue.code
|
|
1462
|
+
}));
|
|
1463
|
+
const validationError = new ValidationError(
|
|
1464
|
+
"Schema validation failed",
|
|
1465
|
+
// eslint-disable-next-line no-undefined
|
|
1466
|
+
void 0,
|
|
1467
|
+
// eslint-disable-next-line no-undefined
|
|
1468
|
+
void 0,
|
|
1469
|
+
// eslint-disable-next-line no-undefined
|
|
1470
|
+
void 0,
|
|
1471
|
+
fieldErrors
|
|
1472
|
+
);
|
|
1473
|
+
throw validationError;
|
|
1474
|
+
}
|
|
1475
|
+
if (error instanceof ValidationError) {
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
throw new ValidationError(`Validation failed: ${error.message || "Unknown error"}`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1441
1482
|
// src/operations/errorEnhancer.ts
|
|
1442
1483
|
async function executeWithContext(operation, context) {
|
|
1443
1484
|
try {
|
|
@@ -1826,35 +1867,49 @@ function createOneWrapper(coordinate, implementation, options = {}) {
|
|
|
1826
1867
|
|
|
1827
1868
|
// src/operations/wrappers/createAllWrapper.ts
|
|
1828
1869
|
var logger10 = logger_default.get("operations", "wrappers", "all");
|
|
1829
|
-
function createAllWrapper(coordinate, implementation,
|
|
1830
|
-
const operationName =
|
|
1831
|
-
return async (query, locations) => {
|
|
1832
|
-
if (
|
|
1833
|
-
logger10.debug(`[${operationName}] Called with:`, { query, locations });
|
|
1870
|
+
function createAllWrapper(coordinate, implementation, wrapperOptions = {}) {
|
|
1871
|
+
const operationName = wrapperOptions.operationName || "all";
|
|
1872
|
+
return async (query, locations, allOptions) => {
|
|
1873
|
+
if (wrapperOptions.debug) {
|
|
1874
|
+
logger10.debug(`[${operationName}] Called with:`, { query, locations, allOptions });
|
|
1834
1875
|
}
|
|
1835
|
-
if (!
|
|
1876
|
+
if (!wrapperOptions.skipValidation) {
|
|
1836
1877
|
validateQuery(query, operationName);
|
|
1837
1878
|
validateLocations(locations, coordinate, operationName);
|
|
1879
|
+
if (allOptions && "limit" in allOptions && allOptions.limit != null) {
|
|
1880
|
+
if (!Number.isInteger(allOptions.limit) || allOptions.limit < 1) {
|
|
1881
|
+
throw new Error(`[${operationName}] limit must be a positive integer, got: ${allOptions.limit}`);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (allOptions && "offset" in allOptions && allOptions.offset != null) {
|
|
1885
|
+
if (!Number.isInteger(allOptions.offset) || allOptions.offset < 0) {
|
|
1886
|
+
throw new Error(`[${operationName}] offset must be a non-negative integer, got: ${allOptions.offset}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1838
1889
|
}
|
|
1839
1890
|
const normalizedQuery = query ?? {};
|
|
1840
1891
|
const normalizedLocations = locations ?? [];
|
|
1841
1892
|
try {
|
|
1842
|
-
const result = await implementation(normalizedQuery, normalizedLocations);
|
|
1843
|
-
if (
|
|
1844
|
-
logger10.debug(`[${operationName}] Result: ${result.length} items`);
|
|
1893
|
+
const result = await implementation(normalizedQuery, normalizedLocations, allOptions);
|
|
1894
|
+
if (wrapperOptions.debug) {
|
|
1895
|
+
logger10.debug(`[${operationName}] Result: ${result.items.length} items, total: ${result.metadata.total}`);
|
|
1845
1896
|
}
|
|
1846
|
-
if (!
|
|
1847
|
-
|
|
1897
|
+
if (!wrapperOptions.skipValidation) {
|
|
1898
|
+
const validatedItems = validatePK(result.items, coordinate.kta[0]);
|
|
1899
|
+
return {
|
|
1900
|
+
items: validatedItems,
|
|
1901
|
+
metadata: result.metadata
|
|
1902
|
+
};
|
|
1848
1903
|
}
|
|
1849
1904
|
return result;
|
|
1850
1905
|
} catch (error) {
|
|
1851
|
-
if (
|
|
1906
|
+
if (wrapperOptions.onError) {
|
|
1852
1907
|
const context = {
|
|
1853
1908
|
operationName,
|
|
1854
|
-
params: [query, locations],
|
|
1909
|
+
params: [query, locations, allOptions],
|
|
1855
1910
|
coordinate
|
|
1856
1911
|
};
|
|
1857
|
-
throw
|
|
1912
|
+
throw wrapperOptions.onError(error, context);
|
|
1858
1913
|
}
|
|
1859
1914
|
throw new Error(
|
|
1860
1915
|
`[${operationName}] Operation failed: ${error.message}`,
|
|
@@ -2082,11 +2137,38 @@ function createRemoveWrapper(coordinate, implementation, options = {}) {
|
|
|
2082
2137
|
|
|
2083
2138
|
// src/operations/wrappers/createFindWrapper.ts
|
|
2084
2139
|
var logger16 = logger_default.get("operations", "wrappers", "find");
|
|
2140
|
+
function isFindOperationResult(value) {
|
|
2141
|
+
return value && typeof value === "object" && "items" in value && "metadata" in value && Array.isArray(value.items) && value.metadata && typeof value.metadata === "object" && "total" in value.metadata;
|
|
2142
|
+
}
|
|
2143
|
+
function applyPagination(items, options) {
|
|
2144
|
+
const total = items.length;
|
|
2145
|
+
const offset = options?.offset ?? 0;
|
|
2146
|
+
const limit = options?.limit;
|
|
2147
|
+
let paginatedItems = items;
|
|
2148
|
+
if (offset > 0) {
|
|
2149
|
+
paginatedItems = paginatedItems.slice(offset);
|
|
2150
|
+
}
|
|
2151
|
+
if (limit != null && limit >= 0) {
|
|
2152
|
+
paginatedItems = paginatedItems.slice(0, limit);
|
|
2153
|
+
}
|
|
2154
|
+
const returned = paginatedItems.length;
|
|
2155
|
+
const hasMore = limit != null && offset + returned < total;
|
|
2156
|
+
return {
|
|
2157
|
+
items: paginatedItems,
|
|
2158
|
+
metadata: {
|
|
2159
|
+
total,
|
|
2160
|
+
returned,
|
|
2161
|
+
offset,
|
|
2162
|
+
limit,
|
|
2163
|
+
hasMore
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2085
2167
|
function createFindWrapper(coordinate, implementation, options = {}) {
|
|
2086
2168
|
const operationName = options.operationName || "find";
|
|
2087
|
-
return async (finder, params, locations) => {
|
|
2169
|
+
return async (finder, params, locations, findOptions) => {
|
|
2088
2170
|
if (options.debug) {
|
|
2089
|
-
logger16.debug(`[${operationName}] Called:`, { finder, params, locations });
|
|
2171
|
+
logger16.debug(`[${operationName}] Called:`, { finder, params, locations, findOptions });
|
|
2090
2172
|
}
|
|
2091
2173
|
if (!options.skipValidation) {
|
|
2092
2174
|
validateFinderName(finder, operationName);
|
|
@@ -2096,19 +2178,36 @@ function createFindWrapper(coordinate, implementation, options = {}) {
|
|
|
2096
2178
|
const normalizedParams = params ?? {};
|
|
2097
2179
|
const normalizedLocations = locations ?? [];
|
|
2098
2180
|
try {
|
|
2099
|
-
const result = await implementation(finder, normalizedParams, normalizedLocations);
|
|
2100
|
-
if (
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2181
|
+
const result = await implementation(finder, normalizedParams, normalizedLocations, findOptions);
|
|
2182
|
+
if (isFindOperationResult(result)) {
|
|
2183
|
+
if (options.debug) {
|
|
2184
|
+
logger16.debug(`[${operationName}] Finder "${finder}" opted-in to pagination, found ${result.items.length} items (total: ${result.metadata.total})`);
|
|
2185
|
+
}
|
|
2186
|
+
if (!options.skipValidation) {
|
|
2187
|
+
const validatedItems = validatePK(result.items, coordinate.kta[0]);
|
|
2188
|
+
return {
|
|
2189
|
+
items: validatedItems,
|
|
2190
|
+
metadata: result.metadata
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
return result;
|
|
2194
|
+
} else {
|
|
2195
|
+
if (options.debug) {
|
|
2196
|
+
logger16.debug(`[${operationName}] Finder "${finder}" using legacy signature, applying post-processing pagination`);
|
|
2197
|
+
}
|
|
2198
|
+
let validatedItems;
|
|
2199
|
+
if (!options.skipValidation) {
|
|
2200
|
+
validatedItems = validatePK(result, coordinate.kta[0]);
|
|
2201
|
+
} else {
|
|
2202
|
+
validatedItems = result;
|
|
2203
|
+
}
|
|
2204
|
+
return applyPagination(validatedItems, findOptions);
|
|
2105
2205
|
}
|
|
2106
|
-
return result;
|
|
2107
2206
|
} catch (error) {
|
|
2108
2207
|
if (options.onError) {
|
|
2109
2208
|
const context = {
|
|
2110
2209
|
operationName,
|
|
2111
|
-
params: [finder, params, locations],
|
|
2210
|
+
params: [finder, params, locations, findOptions],
|
|
2112
2211
|
coordinate
|
|
2113
2212
|
};
|
|
2114
2213
|
throw options.onError(error, context);
|
|
@@ -2440,5 +2539,6 @@ export {
|
|
|
2440
2539
|
validateOperationParams,
|
|
2441
2540
|
validatePK,
|
|
2442
2541
|
validatePriKey,
|
|
2443
|
-
validateQuery
|
|
2542
|
+
validateQuery,
|
|
2543
|
+
validateSchema
|
|
2444
2544
|
};
|
|
@@ -19,6 +19,185 @@ export type CreateOptions<S extends string, L1 extends string = never, L2 extend
|
|
|
19
19
|
key?: never;
|
|
20
20
|
locations: LocKeyArray<L1, L2, L3, L4, L5>;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Base options for pagination operations (all() and find()).
|
|
24
|
+
*
|
|
25
|
+
* Contains the common pagination parameters shared by both operations.
|
|
26
|
+
*
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export interface PaginationOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Maximum number of items to return.
|
|
32
|
+
*
|
|
33
|
+
* - Must be a positive integer (>= 1)
|
|
34
|
+
* - When not provided, returns all matching items
|
|
35
|
+
* - Takes precedence over query.limit when both are specified
|
|
36
|
+
*/
|
|
37
|
+
limit?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Number of items to skip before returning results.
|
|
40
|
+
*
|
|
41
|
+
* - Must be a non-negative integer (>= 0)
|
|
42
|
+
* - Defaults to 0 when not provided
|
|
43
|
+
* - Takes precedence over query.offset when both are specified
|
|
44
|
+
*/
|
|
45
|
+
offset?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Options for the all() operation with pagination support.
|
|
49
|
+
*
|
|
50
|
+
* When provided, these options take precedence over any limit/offset
|
|
51
|
+
* specified in the ItemQuery.
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // Fetch first 50 items
|
|
58
|
+
* const result = await operations.all(query, [], { limit: 50, offset: 0 });
|
|
59
|
+
*
|
|
60
|
+
* // Fetch next page
|
|
61
|
+
* const nextPage = await operations.all(query, [], { limit: 50, offset: 50 });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export interface AllOptions extends PaginationOptions {
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Options for the find() operation with pagination support.
|
|
68
|
+
*
|
|
69
|
+
* When provided, these options take precedence over any limit/offset
|
|
70
|
+
* passed to finder functions.
|
|
71
|
+
*
|
|
72
|
+
* @public
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Find with pagination
|
|
77
|
+
* const findResult = await operations.find('byEmail', { email: 'test@example.com' }, [], { limit: 10 });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export interface FindOptions extends PaginationOptions {
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Metadata about the pagination state for an all() operation.
|
|
84
|
+
*
|
|
85
|
+
* This metadata enables proper pagination UI and logic by providing
|
|
86
|
+
* the total count of matching items before limit/offset are applied.
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export interface PaginationMetadata {
|
|
91
|
+
/**
|
|
92
|
+
* Total count of items matching the query BEFORE limit/offset applied.
|
|
93
|
+
*
|
|
94
|
+
* This represents the complete result set size and is used to:
|
|
95
|
+
* - Display "Showing X of Y results"
|
|
96
|
+
* - Calculate total pages
|
|
97
|
+
* - Determine if more items exist
|
|
98
|
+
*/
|
|
99
|
+
total: number;
|
|
100
|
+
/**
|
|
101
|
+
* Number of items actually returned in this response.
|
|
102
|
+
*
|
|
103
|
+
* This equals `items.length` and is provided for convenience.
|
|
104
|
+
* When offset + returned < total, more items exist.
|
|
105
|
+
*/
|
|
106
|
+
returned: number;
|
|
107
|
+
/**
|
|
108
|
+
* The limit that was applied, if any.
|
|
109
|
+
*
|
|
110
|
+
* - Undefined when no limit was applied
|
|
111
|
+
* - Reflects the effective limit (options.limit ?? query.limit)
|
|
112
|
+
*/
|
|
113
|
+
limit?: number;
|
|
114
|
+
/**
|
|
115
|
+
* The offset that was applied.
|
|
116
|
+
*
|
|
117
|
+
* - 0 when no offset was applied
|
|
118
|
+
* - Reflects the effective offset (options.offset ?? query.offset ?? 0)
|
|
119
|
+
*/
|
|
120
|
+
offset: number;
|
|
121
|
+
/**
|
|
122
|
+
* Convenience field indicating whether more items exist beyond this page.
|
|
123
|
+
*
|
|
124
|
+
* Calculated as: `offset + returned < total`
|
|
125
|
+
*
|
|
126
|
+
* Useful for:
|
|
127
|
+
* - "Load More" buttons
|
|
128
|
+
* - Infinite scroll implementations
|
|
129
|
+
* - "Next Page" button state
|
|
130
|
+
*/
|
|
131
|
+
hasMore: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Base result structure for paginated operations.
|
|
135
|
+
*
|
|
136
|
+
* This structure provides both the items and metadata needed for
|
|
137
|
+
* implementing proper pagination in applications.
|
|
138
|
+
*
|
|
139
|
+
* @template T - The item type being returned
|
|
140
|
+
*
|
|
141
|
+
* @public
|
|
142
|
+
*/
|
|
143
|
+
export interface OperationResult<T> {
|
|
144
|
+
/**
|
|
145
|
+
* Array of items matching the query, with limit/offset applied.
|
|
146
|
+
*/
|
|
147
|
+
items: T[];
|
|
148
|
+
/**
|
|
149
|
+
* Pagination metadata for the result set.
|
|
150
|
+
*/
|
|
151
|
+
metadata: PaginationMetadata;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Result structure for the all() operation with pagination support.
|
|
155
|
+
*
|
|
156
|
+
* This structure provides both the items and metadata needed for
|
|
157
|
+
* implementing proper pagination in applications.
|
|
158
|
+
*
|
|
159
|
+
* @template T - The item type being returned
|
|
160
|
+
*
|
|
161
|
+
* @public
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* const result = await operations.all(query, [], { limit: 50, offset: 0 });
|
|
166
|
+
*
|
|
167
|
+
* console.log(`Showing ${result.metadata.returned} of ${result.metadata.total}`);
|
|
168
|
+
* // "Showing 50 of 1234"
|
|
169
|
+
*
|
|
170
|
+
* if (result.metadata.hasMore) {
|
|
171
|
+
* // Load next page
|
|
172
|
+
* }
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export interface AllOperationResult<T> extends OperationResult<T> {
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Result structure for the find() operation with pagination support.
|
|
179
|
+
*
|
|
180
|
+
* This structure provides both the items and metadata needed for
|
|
181
|
+
* implementing proper pagination in find operations.
|
|
182
|
+
*
|
|
183
|
+
* @template T - The item type being returned
|
|
184
|
+
*
|
|
185
|
+
* @public
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const result = await operations.find('byEmail', { email: 'test@example.com' }, [], { limit: 10 });
|
|
190
|
+
*
|
|
191
|
+
* console.log(`Found ${result.metadata.returned} of ${result.metadata.total} results`);
|
|
192
|
+
* // "Found 10 of 25 results"
|
|
193
|
+
*
|
|
194
|
+
* if (result.metadata.hasMore) {
|
|
195
|
+
* // Load next page
|
|
196
|
+
* }
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export interface FindOperationResult<T> extends OperationResult<T> {
|
|
200
|
+
}
|
|
22
201
|
/**
|
|
23
202
|
* Options for update operations across all Fjell libraries.
|
|
24
203
|
*
|
|
@@ -97,25 +276,38 @@ export interface UpdateOptions {
|
|
|
97
276
|
*/
|
|
98
277
|
export interface Operations<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> {
|
|
99
278
|
/**
|
|
100
|
-
* Retrieves all items matching the query.
|
|
279
|
+
* Retrieves all items matching the query with optional pagination.
|
|
101
280
|
*
|
|
102
|
-
* @param query - Optional query to filter items
|
|
281
|
+
* @param query - Optional query to filter items (may include limit/offset for backwards compatibility)
|
|
103
282
|
* @param locations - Optional location hierarchy to scope the query
|
|
104
|
-
* @
|
|
283
|
+
* @param options - Optional pagination options (takes precedence over query limit/offset)
|
|
284
|
+
* @returns Result containing items and pagination metadata
|
|
105
285
|
*
|
|
106
|
-
* @example
|
|
286
|
+
* @example Get all items
|
|
107
287
|
* ```typescript
|
|
108
|
-
*
|
|
109
|
-
*
|
|
288
|
+
* const result = await operations.all();
|
|
289
|
+
* // result.items = all items
|
|
290
|
+
* // result.metadata.total = items.length
|
|
291
|
+
* ```
|
|
110
292
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
293
|
+
* @example Get paginated items
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const result = await operations.all({}, [], { limit: 50, offset: 0 });
|
|
296
|
+
* // result.items = first 50 items
|
|
297
|
+
* // result.metadata.total = total matching count
|
|
298
|
+
* // result.metadata.hasMore = true if more exist
|
|
299
|
+
* ```
|
|
113
300
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
301
|
+
* @example Get items in specific location with pagination
|
|
302
|
+
* ```typescript
|
|
303
|
+
* const result = await operations.all(
|
|
304
|
+
* {},
|
|
305
|
+
* [{kt: 'post', lk: 'post-123'}],
|
|
306
|
+
* { limit: 20, offset: 0 }
|
|
307
|
+
* );
|
|
116
308
|
* ```
|
|
117
309
|
*/
|
|
118
|
-
all(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V
|
|
310
|
+
all(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: AllOptions): Promise<AllOperationResult<V>>;
|
|
119
311
|
/**
|
|
120
312
|
* Retrieves the first item matching the query.
|
|
121
313
|
*
|
|
@@ -209,27 +401,36 @@ export interface Operations<V extends Item<S, L1, L2, L3, L4, L5>, S extends str
|
|
|
209
401
|
*/
|
|
210
402
|
remove(key: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>): Promise<V | void>;
|
|
211
403
|
/**
|
|
212
|
-
* Executes a finder method by name.
|
|
404
|
+
* Executes a finder method by name with optional pagination support.
|
|
405
|
+
*
|
|
406
|
+
* Supports hybrid pagination approach:
|
|
407
|
+
* - If finder returns FindOperationResult<V>, uses it directly (opt-in)
|
|
408
|
+
* - If finder returns V[], framework applies post-processing pagination
|
|
213
409
|
*
|
|
214
410
|
* @param finder - Name of the finder method
|
|
215
411
|
* @param params - Parameters for the finder
|
|
216
412
|
* @param locations - Optional location hierarchy to scope the query
|
|
217
|
-
* @
|
|
413
|
+
* @param options - Optional pagination options (limit, offset)
|
|
414
|
+
* @returns Result containing items and pagination metadata
|
|
218
415
|
*
|
|
219
416
|
* @example
|
|
220
417
|
* ```typescript
|
|
221
|
-
* // Find users by email
|
|
222
|
-
* const
|
|
418
|
+
* // Find users by email (no pagination)
|
|
419
|
+
* const result = await operations.find('byEmail', { email: 'alice@example.com' });
|
|
420
|
+
* const users = result.items;
|
|
223
421
|
*
|
|
224
|
-
* // Find
|
|
225
|
-
* const
|
|
422
|
+
* // Find with pagination
|
|
423
|
+
* const result = await operations.find(
|
|
226
424
|
* 'byAuthor',
|
|
227
425
|
* { author: 'alice' },
|
|
228
|
-
* [{kt: 'post', lk: 'post-123'}]
|
|
426
|
+
* [{kt: 'post', lk: 'post-123'}],
|
|
427
|
+
* { limit: 10, offset: 0 }
|
|
229
428
|
* );
|
|
429
|
+
* const comments = result.items;
|
|
430
|
+
* const total = result.metadata.total;
|
|
230
431
|
* ```
|
|
231
432
|
*/
|
|
232
|
-
find(finder: string, params?: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V
|
|
433
|
+
find(finder: string, params?: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<FindOperationResult<V>>;
|
|
233
434
|
/**
|
|
234
435
|
* Executes a finder method and returns the first result.
|
|
235
436
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Item } from "../items";
|
|
2
2
|
import { ComKey, LocKeyArray } from "../keys";
|
|
3
3
|
import { ItemQuery } from "../item/ItemQuery";
|
|
4
|
-
import { AffectedKeys, OperationParams, Operations } from "./Operations";
|
|
4
|
+
import { AffectedKeys, AllOperationResult, AllOptions, FindOperationResult, FindOptions, OperationParams, Operations } from "./Operations";
|
|
5
5
|
/**
|
|
6
6
|
* Contained Operations interface - specialized for contained (hierarchical) items only.
|
|
7
7
|
*
|
|
@@ -47,9 +47,9 @@ import { AffectedKeys, OperationParams, Operations } from "./Operations";
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
export interface ContainedOperations<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> extends Omit<Operations<V, S, L1, L2, L3, L4, L5>, 'get' | 'update' | 'remove' | 'upsert' | 'create' | 'action' | 'facet' | 'allAction' | 'allFacet' | 'all' | 'one' | 'find' | 'findOne'> {
|
|
50
|
-
all(query: ItemQuery | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V
|
|
50
|
+
all(query: ItemQuery | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | [], allOptions?: AllOptions): Promise<AllOperationResult<V>>;
|
|
51
51
|
one(query: ItemQuery | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
|
|
52
|
-
find(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V
|
|
52
|
+
find(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<FindOperationResult<V>>;
|
|
53
53
|
findOne(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
|
|
54
54
|
create(item: Partial<Item<S, L1, L2, L3, L4, L5>>, options?: {
|
|
55
55
|
locations?: LocKeyArray<L1, L2, L3, L4, L5>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Item } from "../items";
|
|
2
2
|
import { ComKey, LocKeyArray, PriKey } from "../keys";
|
|
3
3
|
import { ItemQuery } from "../item/ItemQuery";
|
|
4
|
-
import { AffectedKeys, CreateOptions, OperationParams, UpdateOptions } from "./Operations";
|
|
4
|
+
import { AffectedKeys, AllOperationResult, AllOptions, CreateOptions, FindOperationResult, FindOptions, OperationParams, UpdateOptions } from "./Operations";
|
|
5
5
|
/**
|
|
6
6
|
* Get method signature - retrieves single item by key
|
|
7
7
|
*/
|
|
@@ -33,10 +33,34 @@ export interface UpsertMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends s
|
|
|
33
33
|
(key: PriKey<S> | ComKey<S, L1, L2, L3, L4, L5>, item: Partial<Item<S, L1, L2, L3, L4, L5>>, locations?: LocKeyArray<L1, L2, L3, L4, L5>, options?: UpdateOptions): Promise<V>;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* All method signature - retrieves all items matching query
|
|
36
|
+
* All method signature - retrieves all items matching query with optional pagination.
|
|
37
|
+
*
|
|
38
|
+
* @param query - Optional query to filter items (may include limit/offset for backwards compatibility)
|
|
39
|
+
* @param locations - Optional location hierarchy to scope the query
|
|
40
|
+
* @param options - Optional pagination options (takes precedence over query limit/offset)
|
|
41
|
+
* @returns Result containing items and pagination metadata
|
|
42
|
+
*
|
|
43
|
+
* @example Without options (backwards compatible)
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const result = await operations.all({ compoundCondition: {...} });
|
|
46
|
+
* // result.items = [...all matching items...]
|
|
47
|
+
* // result.metadata.total = result.items.length
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @example With options (new pattern)
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const result = await operations.all(
|
|
53
|
+
* { compoundCondition: {...} },
|
|
54
|
+
* [],
|
|
55
|
+
* { limit: 50, offset: 0 }
|
|
56
|
+
* );
|
|
57
|
+
* // result.items = [...first 50 items...]
|
|
58
|
+
* // result.metadata.total = total matching count
|
|
59
|
+
* // result.metadata.hasMore = true if more items exist
|
|
60
|
+
* ```
|
|
37
61
|
*/
|
|
38
62
|
export interface AllMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> {
|
|
39
|
-
(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V
|
|
63
|
+
(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: AllOptions): Promise<AllOperationResult<V>>;
|
|
40
64
|
}
|
|
41
65
|
/**
|
|
42
66
|
* One method signature - retrieves first item matching query
|
|
@@ -45,10 +69,29 @@ export interface OneMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends stri
|
|
|
45
69
|
(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
|
|
46
70
|
}
|
|
47
71
|
/**
|
|
48
|
-
* Find method signature - finds multiple items using finder
|
|
72
|
+
* Find method signature - finds multiple items using finder with optional pagination.
|
|
73
|
+
*
|
|
74
|
+
* Supports hybrid approach:
|
|
75
|
+
* - If finder returns FindOperationResult<V>, uses it directly (opt-in)
|
|
76
|
+
* - If finder returns V[], framework applies post-processing pagination
|
|
77
|
+
*
|
|
78
|
+
* @param finder - Name of the finder method
|
|
79
|
+
* @param params - Parameters for the finder
|
|
80
|
+
* @param locations - Optional location hierarchy to scope the query
|
|
81
|
+
* @param options - Optional pagination options (limit, offset)
|
|
82
|
+
* @returns Result containing items and pagination metadata
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* // Without pagination (returns all results)
|
|
87
|
+
* const result = await operations.find('byEmail', { email: 'test@example.com' });
|
|
88
|
+
*
|
|
89
|
+
* // With pagination
|
|
90
|
+
* const result = await operations.find('byEmail', { email: 'test@example.com' }, [], { limit: 10, offset: 0 });
|
|
91
|
+
* ```
|
|
49
92
|
*/
|
|
50
93
|
export interface FindMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> {
|
|
51
|
-
(finder: string, params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V
|
|
94
|
+
(finder: string, params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<FindOperationResult<V>>;
|
|
52
95
|
}
|
|
53
96
|
/**
|
|
54
97
|
* FindOne method signature - finds single item using finder
|
|
@@ -57,17 +100,44 @@ export interface FindOneMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends
|
|
|
57
100
|
(finder: string, params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
|
|
58
101
|
}
|
|
59
102
|
/**
|
|
60
|
-
* Finder method signature - finds multiple items
|
|
103
|
+
* Finder method signature - finds multiple items.
|
|
61
104
|
*
|
|
62
|
-
*
|
|
105
|
+
* Supports hybrid approach for pagination:
|
|
106
|
+
* - **Legacy signature**: Return `Promise<V[]>` - framework applies post-processing pagination
|
|
107
|
+
* - **Opt-in signature**: Return `Promise<FindOperationResult<V>>` - finder handles pagination at source
|
|
108
|
+
*
|
|
109
|
+
* @example Legacy finder (framework handles pagination)
|
|
63
110
|
* ```typescript
|
|
64
111
|
* const byEmailFinder: FinderMethod<User, 'user'> = async (params) => {
|
|
65
112
|
* return await database.findUsers({ email: params.email });
|
|
66
113
|
* };
|
|
67
114
|
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example Opt-in finder (finder handles pagination)
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const byEmailFinder: FinderMethod<User, 'user'> = async (params, locations, options) => {
|
|
119
|
+
* const query = buildQuery({ email: params.email });
|
|
120
|
+
* const total = await database.count(query);
|
|
121
|
+
*
|
|
122
|
+
* if (options?.offset) query.offset(options.offset);
|
|
123
|
+
* if (options?.limit) query.limit(options.limit);
|
|
124
|
+
*
|
|
125
|
+
* const items = await database.find(query);
|
|
126
|
+
* return {
|
|
127
|
+
* items,
|
|
128
|
+
* metadata: {
|
|
129
|
+
* total,
|
|
130
|
+
* returned: items.length,
|
|
131
|
+
* offset: options?.offset ?? 0,
|
|
132
|
+
* limit: options?.limit,
|
|
133
|
+
* hasMore: (options?.offset ?? 0) + items.length < total
|
|
134
|
+
* }
|
|
135
|
+
* };
|
|
136
|
+
* };
|
|
137
|
+
* ```
|
|
68
138
|
*/
|
|
69
139
|
export interface FinderMethod<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never> {
|
|
70
|
-
(params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]
|
|
140
|
+
(params: OperationParams, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: FindOptions): Promise<V[] | FindOperationResult<V>>;
|
|
71
141
|
}
|
|
72
142
|
/**
|
|
73
143
|
* Action operation method signature - executes action on specific item by key
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Item } from "../items";
|
|
2
2
|
import { PriKey } from "../keys";
|
|
3
3
|
import { ItemQuery } from "../item/ItemQuery";
|
|
4
|
-
import { AffectedKeys, OperationParams, Operations } from "./Operations";
|
|
4
|
+
import { AffectedKeys, AllOperationResult, AllOptions, FindOperationResult, FindOptions, OperationParams, Operations } from "./Operations";
|
|
5
5
|
/**
|
|
6
6
|
* Primary Operations interface - specialized for primary (top-level) items only.
|
|
7
7
|
*
|
|
@@ -39,9 +39,9 @@ import { AffectedKeys, OperationParams, Operations } from "./Operations";
|
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
41
|
export interface PrimaryOperations<V extends Item<S>, S extends string> extends Omit<Operations<V, S>, 'all' | 'one' | 'create' | 'get' | 'update' | 'upsert' | 'remove' | 'find' | 'findOne' | 'action' | 'allAction' | 'facet' | 'allFacet'> {
|
|
42
|
-
all(query?: ItemQuery): Promise<V
|
|
42
|
+
all(query?: ItemQuery, locations?: [], allOptions?: AllOptions): Promise<AllOperationResult<V>>;
|
|
43
43
|
one(query?: ItemQuery): Promise<V | null>;
|
|
44
|
-
find(finder: string, params?: OperationParams): Promise<V
|
|
44
|
+
find(finder: string, params?: OperationParams, locations?: [], options?: FindOptions): Promise<FindOperationResult<V>>;
|
|
45
45
|
findOne(finder: string, params?: OperationParams): Promise<V | null>;
|
|
46
46
|
create(item: Partial<Item<S>>, options?: {
|
|
47
47
|
key?: PriKey<S>;
|
|
@@ -19,10 +19,10 @@ import type { WrapperOptions } from "./types";
|
|
|
19
19
|
* ```typescript
|
|
20
20
|
* const all = createAllWrapper(
|
|
21
21
|
* coordinate,
|
|
22
|
-
* async (query, locations) => {
|
|
23
|
-
* return await database.findAll(query, locations);
|
|
22
|
+
* async (query, locations, options) => {
|
|
23
|
+
* return await database.findAll(query, locations, options);
|
|
24
24
|
* }
|
|
25
25
|
* );
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
|
-
export declare function createAllWrapper<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never>(coordinate: Coordinate<S, L1, L2, L3, L4, L5>, implementation: AllMethod<V, S, L1, L2, L3, L4, L5>,
|
|
28
|
+
export declare function createAllWrapper<V extends Item<S, L1, L2, L3, L4, L5>, S extends string, L1 extends string = never, L2 extends string = never, L3 extends string = never, L4 extends string = never, L5 extends string = never>(coordinate: Coordinate<S, L1, L2, L3, L4, L5>, implementation: AllMethod<V, S, L1, L2, L3, L4, L5>, wrapperOptions?: WrapperOptions): AllMethod<V, S, L1, L2, L3, L4, L5>;
|
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wrapper for find() operation
|
|
3
3
|
*
|
|
4
|
-
* Provides automatic validation for find() operation parameters
|
|
4
|
+
* Provides automatic validation for find() operation parameters and supports
|
|
5
|
+
* hybrid pagination approach:
|
|
6
|
+
* - If finder returns FindOperationResult<V>, uses it directly (opt-in)
|
|
7
|
+
* - If finder returns V[], applies post-processing pagination
|
|
5
8
|
*/
|
|
6
9
|
import type { Item } from "../../items";
|
|
7
10
|
import type { Coordinate } from "../../Coordinate";
|
|
8
11
|
import type { FindMethod } from "../methods";
|
|
9
12
|
import type { WrapperOptions } from "./types";
|
|
10
13
|
/**
|
|
11
|
-
* Creates a wrapped find() method with automatic parameter validation.
|
|
14
|
+
* Creates a wrapped find() method with automatic parameter validation and hybrid pagination support.
|
|
12
15
|
*
|
|
13
16
|
* @param coordinate - The coordinate defining the item hierarchy
|
|
14
17
|
* @param implementation - The core logic for the operation
|
|
15
18
|
* @param options - Optional configuration
|
|
16
|
-
* @returns A fully validated find() method
|
|
19
|
+
* @returns A fully validated find() method that returns FindOperationResult<V>
|
|
17
20
|
*
|
|
18
21
|
* @example
|
|
19
22
|
* ```typescript
|
|
20
23
|
* const find = createFindWrapper(
|
|
21
24
|
* coordinate,
|
|
22
|
-
* async (finder, params, locations) => {
|
|
23
|
-
*
|
|
25
|
+
* async (finder, params, locations, findOptions) => {
|
|
26
|
+
* const finderMethod = finders[finder];
|
|
27
|
+
* const result = await finderMethod(params, locations, findOptions);
|
|
28
|
+
* // Framework handles hybrid detection and post-processing if needed
|
|
29
|
+
* return result;
|
|
24
30
|
* }
|
|
25
31
|
* );
|
|
26
32
|
* ```
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* - Items (Item key type validation)
|
|
8
8
|
* - Queries (ItemQuery validation)
|
|
9
9
|
* - Operation parameters (OperationParams validation)
|
|
10
|
+
* - Schema validation (Zod, Yup, etc.)
|
|
10
11
|
*/
|
|
11
|
-
export type { ValidationOptions, ValidationResult } from './types';
|
|
12
|
+
export type { ValidationOptions, ValidationResult, SchemaValidator } from './types';
|
|
12
13
|
export { validateLocations, isValidLocations } from './LocationValidator';
|
|
13
14
|
export { validateKey, validatePriKey, validateComKey } from './KeyValidator';
|
|
14
15
|
export { validatePK, validateKeys } from './ItemValidator';
|
|
15
16
|
export { validateQuery, validateOperationParams, validateFinderName, validateActionName, validateFacetName } from './QueryValidator';
|
|
17
|
+
export { validateSchema } from './schema';
|
package/dist/validation/index.js
CHANGED
|
@@ -485,6 +485,96 @@ Received: "${facet}"`
|
|
|
485
485
|
}
|
|
486
486
|
logger5.debug(`Facet name validation passed for ${operation}`, { facet });
|
|
487
487
|
};
|
|
488
|
+
|
|
489
|
+
// src/errors/ActionError.ts
|
|
490
|
+
var ActionError = class extends Error {
|
|
491
|
+
constructor(errorInfo, cause) {
|
|
492
|
+
super(errorInfo.message);
|
|
493
|
+
this.errorInfo = errorInfo;
|
|
494
|
+
this.name = "ActionError";
|
|
495
|
+
this.cause = cause;
|
|
496
|
+
if (!this.errorInfo.technical) {
|
|
497
|
+
this.errorInfo.technical = { timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
498
|
+
}
|
|
499
|
+
if (!this.errorInfo.technical.timestamp) {
|
|
500
|
+
this.errorInfo.technical.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
toJSON() {
|
|
504
|
+
return this.errorInfo;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/errors/ValidationError.ts
|
|
509
|
+
var ValidationError = class extends ActionError {
|
|
510
|
+
fieldErrors;
|
|
511
|
+
constructor(message, validOptions, suggestedAction, conflictingValue, fieldErrors) {
|
|
512
|
+
super({
|
|
513
|
+
code: "VALIDATION_ERROR",
|
|
514
|
+
message,
|
|
515
|
+
operation: { type: "create", name: "", params: {} },
|
|
516
|
+
// Will be filled by wrapper
|
|
517
|
+
context: { itemType: "" },
|
|
518
|
+
// Will be filled by wrapper
|
|
519
|
+
details: {
|
|
520
|
+
validOptions,
|
|
521
|
+
suggestedAction,
|
|
522
|
+
retryable: true,
|
|
523
|
+
conflictingValue,
|
|
524
|
+
fieldErrors
|
|
525
|
+
},
|
|
526
|
+
technical: {
|
|
527
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
this.fieldErrors = fieldErrors;
|
|
531
|
+
if (fieldErrors) {
|
|
532
|
+
if (!this.errorInfo.details) this.errorInfo.details = {};
|
|
533
|
+
this.errorInfo.details.fieldErrors = fieldErrors;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/validation/schema.ts
|
|
539
|
+
async function validateSchema(data, schema) {
|
|
540
|
+
if (!schema) {
|
|
541
|
+
return data;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
if (schema.parseAsync) {
|
|
545
|
+
return await schema.parseAsync(data);
|
|
546
|
+
}
|
|
547
|
+
const result = schema.safeParse(data);
|
|
548
|
+
if (result.success) {
|
|
549
|
+
return result.data;
|
|
550
|
+
} else {
|
|
551
|
+
throw result.error;
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error && Array.isArray(error.issues)) {
|
|
555
|
+
const fieldErrors = error.issues.map((issue) => ({
|
|
556
|
+
path: issue.path,
|
|
557
|
+
message: issue.message,
|
|
558
|
+
code: issue.code
|
|
559
|
+
}));
|
|
560
|
+
const validationError = new ValidationError(
|
|
561
|
+
"Schema validation failed",
|
|
562
|
+
// eslint-disable-next-line no-undefined
|
|
563
|
+
void 0,
|
|
564
|
+
// eslint-disable-next-line no-undefined
|
|
565
|
+
void 0,
|
|
566
|
+
// eslint-disable-next-line no-undefined
|
|
567
|
+
void 0,
|
|
568
|
+
fieldErrors
|
|
569
|
+
);
|
|
570
|
+
throw validationError;
|
|
571
|
+
}
|
|
572
|
+
if (error instanceof ValidationError) {
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
throw new ValidationError(`Validation failed: ${error.message || "Unknown error"}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
488
578
|
export {
|
|
489
579
|
isValidLocations,
|
|
490
580
|
validateActionName,
|
|
@@ -497,5 +587,6 @@ export {
|
|
|
497
587
|
validateOperationParams,
|
|
498
588
|
validatePK,
|
|
499
589
|
validatePriKey,
|
|
500
|
-
validateQuery
|
|
590
|
+
validateQuery,
|
|
591
|
+
validateSchema
|
|
501
592
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SchemaValidator } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Validates data against a schema validator (Zod, Yup, etc.)
|
|
4
|
+
*
|
|
5
|
+
* Supports both synchronous and asynchronous validation.
|
|
6
|
+
* Transforms validator errors into Fjell ValidationError with FieldError[].
|
|
7
|
+
*
|
|
8
|
+
* @template T - The validated/parsed type
|
|
9
|
+
* @param data - The data to validate
|
|
10
|
+
* @param schema - Optional schema validator
|
|
11
|
+
* @returns Promise resolving to validated/parsed data
|
|
12
|
+
* @throws ValidationError if validation fails
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { validateSchema } from '@fjell/core/validation';
|
|
17
|
+
* import { z } from 'zod';
|
|
18
|
+
*
|
|
19
|
+
* const schema = z.object({ name: z.string() });
|
|
20
|
+
* const validated = await validateSchema({ name: 'Alice' }, schema);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateSchema<T>(data: unknown, schema?: SchemaValidator<T>): Promise<T>;
|
|
@@ -36,3 +36,34 @@ export interface ValidationResult {
|
|
|
36
36
|
*/
|
|
37
37
|
context?: Record<string, unknown>;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Generic interface for schema validators (Zod, Yup, Joi, etc.)
|
|
41
|
+
*
|
|
42
|
+
* This interface enables duck typing - any validator that implements
|
|
43
|
+
* these methods will work with validateSchema.
|
|
44
|
+
*
|
|
45
|
+
* @template T - The validated/parsed type
|
|
46
|
+
*/
|
|
47
|
+
export interface SchemaValidator<T> {
|
|
48
|
+
/**
|
|
49
|
+
* Synchronous parse method.
|
|
50
|
+
* Should throw an error if invalid, or return the parsed data.
|
|
51
|
+
*/
|
|
52
|
+
parse: (data: unknown) => T;
|
|
53
|
+
/**
|
|
54
|
+
* Safe parse method that returns a result object instead of throwing.
|
|
55
|
+
* Used as fallback if parseAsync is not available.
|
|
56
|
+
*/
|
|
57
|
+
safeParse: (data: unknown) => {
|
|
58
|
+
success: true;
|
|
59
|
+
data: T;
|
|
60
|
+
} | {
|
|
61
|
+
success: false;
|
|
62
|
+
error: any;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Optional asynchronous parse method.
|
|
66
|
+
* Preferred over parse/safeParse if available.
|
|
67
|
+
*/
|
|
68
|
+
parseAsync?: (data: unknown) => Promise<T>;
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjell/core",
|
|
3
3
|
"description": "Core Item and Key Framework for Fjell",
|
|
4
|
-
"version": "4.4.
|
|
4
|
+
"version": "4.4.65",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"core",
|
|
7
7
|
"fjell"
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"docs:test": "cd docs && npm run test"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@fjell/logging": "^4.4.
|
|
44
|
+
"@fjell/logging": "^4.4.59",
|
|
45
45
|
"deepmerge": "^4.3.1",
|
|
46
46
|
"luxon": "^3.7.2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/eslintrc": "^3.3.1",
|
|
50
50
|
"@eslint/js": "^9.39.1",
|
|
51
|
-
"@fjell/common-config": "^1.1.
|
|
51
|
+
"@fjell/common-config": "^1.1.31",
|
|
52
52
|
"@swc/core": "^1.15.2",
|
|
53
53
|
"@tsconfig/recommended": "^1.0.13",
|
|
54
54
|
"@types/luxon": "^3.7.1",
|