@fjell/core 4.4.62 → 4.4.64

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.
@@ -36,6 +36,7 @@ export interface ErrorInfo {
36
36
  retryable?: boolean;
37
37
  conflictingValue?: any;
38
38
  expectedValue?: any;
39
+ fieldErrors?: any[];
39
40
  };
40
41
  technical?: {
41
42
  timestamp: string;
@@ -1,4 +1,10 @@
1
1
  import { ActionError } from './ActionError';
2
+ export interface FieldError {
3
+ path: (string | number)[];
4
+ message: string;
5
+ code: string;
6
+ }
2
7
  export declare class ValidationError extends ActionError {
3
- constructor(message: string, validOptions?: string[], suggestedAction?: string, conflictingValue?: any);
8
+ fieldErrors?: FieldError[];
9
+ constructor(message: string, validOptions?: string[], suggestedAction?: string, conflictingValue?: any, fieldErrors?: FieldError[]);
4
10
  }
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, AllOptions, PaginationMetadata, AllOperationResult } 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
@@ -1300,7 +1300,8 @@ var ActionError = class extends Error {
1300
1300
 
1301
1301
  // src/errors/ValidationError.ts
1302
1302
  var ValidationError = class extends ActionError {
1303
- constructor(message, validOptions, suggestedAction, conflictingValue) {
1303
+ fieldErrors;
1304
+ constructor(message, validOptions, suggestedAction, conflictingValue, fieldErrors) {
1304
1305
  super({
1305
1306
  code: "VALIDATION_ERROR",
1306
1307
  message,
@@ -1312,12 +1313,18 @@ var ValidationError = class extends ActionError {
1312
1313
  validOptions,
1313
1314
  suggestedAction,
1314
1315
  retryable: true,
1315
- conflictingValue
1316
+ conflictingValue,
1317
+ fieldErrors
1316
1318
  },
1317
1319
  technical: {
1318
1320
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1319
1321
  }
1320
1322
  });
1323
+ this.fieldErrors = fieldErrors;
1324
+ if (fieldErrors) {
1325
+ if (!this.errorInfo.details) this.errorInfo.details = {};
1326
+ this.errorInfo.details.fieldErrors = fieldErrors;
1327
+ }
1321
1328
  }
1322
1329
  };
1323
1330
 
@@ -1819,35 +1826,49 @@ function createOneWrapper(coordinate, implementation, options = {}) {
1819
1826
 
1820
1827
  // src/operations/wrappers/createAllWrapper.ts
1821
1828
  var logger10 = logger_default.get("operations", "wrappers", "all");
1822
- function createAllWrapper(coordinate, implementation, options = {}) {
1823
- const operationName = options.operationName || "all";
1824
- return async (query, locations) => {
1825
- if (options.debug) {
1826
- logger10.debug(`[${operationName}] Called with:`, { query, locations });
1829
+ function createAllWrapper(coordinate, implementation, wrapperOptions = {}) {
1830
+ const operationName = wrapperOptions.operationName || "all";
1831
+ return async (query, locations, allOptions) => {
1832
+ if (wrapperOptions.debug) {
1833
+ logger10.debug(`[${operationName}] Called with:`, { query, locations, allOptions });
1827
1834
  }
1828
- if (!options.skipValidation) {
1835
+ if (!wrapperOptions.skipValidation) {
1829
1836
  validateQuery(query, operationName);
1830
1837
  validateLocations(locations, coordinate, operationName);
1838
+ if (allOptions && "limit" in allOptions && allOptions.limit != null) {
1839
+ if (!Number.isInteger(allOptions.limit) || allOptions.limit < 1) {
1840
+ throw new Error(`[${operationName}] limit must be a positive integer, got: ${allOptions.limit}`);
1841
+ }
1842
+ }
1843
+ if (allOptions && "offset" in allOptions && allOptions.offset != null) {
1844
+ if (!Number.isInteger(allOptions.offset) || allOptions.offset < 0) {
1845
+ throw new Error(`[${operationName}] offset must be a non-negative integer, got: ${allOptions.offset}`);
1846
+ }
1847
+ }
1831
1848
  }
1832
1849
  const normalizedQuery = query ?? {};
1833
1850
  const normalizedLocations = locations ?? [];
1834
1851
  try {
1835
- const result = await implementation(normalizedQuery, normalizedLocations);
1836
- if (options.debug) {
1837
- logger10.debug(`[${operationName}] Result: ${result.length} items`);
1852
+ const result = await implementation(normalizedQuery, normalizedLocations, allOptions);
1853
+ if (wrapperOptions.debug) {
1854
+ logger10.debug(`[${operationName}] Result: ${result.items.length} items, total: ${result.metadata.total}`);
1838
1855
  }
1839
- if (!options.skipValidation) {
1840
- return validatePK(result, coordinate.kta[0]);
1856
+ if (!wrapperOptions.skipValidation) {
1857
+ const validatedItems = validatePK(result.items, coordinate.kta[0]);
1858
+ return {
1859
+ items: validatedItems,
1860
+ metadata: result.metadata
1861
+ };
1841
1862
  }
1842
1863
  return result;
1843
1864
  } catch (error) {
1844
- if (options.onError) {
1865
+ if (wrapperOptions.onError) {
1845
1866
  const context = {
1846
1867
  operationName,
1847
- params: [query, locations],
1868
+ params: [query, locations, allOptions],
1848
1869
  coordinate
1849
1870
  };
1850
- throw options.onError(error, context);
1871
+ throw wrapperOptions.onError(error, context);
1851
1872
  }
1852
1873
  throw new Error(
1853
1874
  `[${operationName}] Operation failed: ${error.message}`,
@@ -19,6 +19,124 @@ 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
+ * Options for the all() operation to control pagination.
24
+ *
25
+ * When provided, these options take precedence over any limit/offset
26
+ * specified in the ItemQuery.
27
+ *
28
+ * @public
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // Fetch first 50 items
33
+ * const result = await operations.all(query, [], { limit: 50, offset: 0 });
34
+ *
35
+ * // Fetch next page
36
+ * const nextPage = await operations.all(query, [], { limit: 50, offset: 50 });
37
+ * ```
38
+ */
39
+ export interface AllOptions {
40
+ /**
41
+ * Maximum number of items to return.
42
+ *
43
+ * - Must be a positive integer (>= 1)
44
+ * - When not provided, returns all matching items
45
+ * - Takes precedence over query.limit when both are specified
46
+ */
47
+ limit?: number;
48
+ /**
49
+ * Number of items to skip before returning results.
50
+ *
51
+ * - Must be a non-negative integer (>= 0)
52
+ * - Defaults to 0 when not provided
53
+ * - Takes precedence over query.offset when both are specified
54
+ */
55
+ offset?: number;
56
+ }
57
+ /**
58
+ * Metadata about the pagination state for an all() operation.
59
+ *
60
+ * This metadata enables proper pagination UI and logic by providing
61
+ * the total count of matching items before limit/offset are applied.
62
+ *
63
+ * @public
64
+ */
65
+ export interface PaginationMetadata {
66
+ /**
67
+ * Total count of items matching the query BEFORE limit/offset applied.
68
+ *
69
+ * This represents the complete result set size and is used to:
70
+ * - Display "Showing X of Y results"
71
+ * - Calculate total pages
72
+ * - Determine if more items exist
73
+ */
74
+ total: number;
75
+ /**
76
+ * Number of items actually returned in this response.
77
+ *
78
+ * This equals `items.length` and is provided for convenience.
79
+ * When offset + returned < total, more items exist.
80
+ */
81
+ returned: number;
82
+ /**
83
+ * The limit that was applied, if any.
84
+ *
85
+ * - Undefined when no limit was applied
86
+ * - Reflects the effective limit (options.limit ?? query.limit)
87
+ */
88
+ limit?: number;
89
+ /**
90
+ * The offset that was applied.
91
+ *
92
+ * - 0 when no offset was applied
93
+ * - Reflects the effective offset (options.offset ?? query.offset ?? 0)
94
+ */
95
+ offset: number;
96
+ /**
97
+ * Convenience field indicating whether more items exist beyond this page.
98
+ *
99
+ * Calculated as: `offset + returned < total`
100
+ *
101
+ * Useful for:
102
+ * - "Load More" buttons
103
+ * - Infinite scroll implementations
104
+ * - "Next Page" button state
105
+ */
106
+ hasMore: boolean;
107
+ }
108
+ /**
109
+ * Result structure for the all() operation with pagination support.
110
+ *
111
+ * This structure provides both the items and metadata needed for
112
+ * implementing proper pagination in applications.
113
+ *
114
+ * @template T - The item type being returned
115
+ *
116
+ * @public
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * const result = await operations.all(query, [], { limit: 50, offset: 0 });
121
+ *
122
+ * console.log(`Showing ${result.metadata.returned} of ${result.metadata.total}`);
123
+ * // "Showing 50 of 1234"
124
+ *
125
+ * if (result.metadata.hasMore) {
126
+ * // Load next page
127
+ * }
128
+ * ```
129
+ */
130
+ export interface AllOperationResult<T> {
131
+ /**
132
+ * Array of items matching the query, with limit/offset applied.
133
+ */
134
+ items: T[];
135
+ /**
136
+ * Pagination metadata for the result set.
137
+ */
138
+ metadata: PaginationMetadata;
139
+ }
22
140
  /**
23
141
  * Options for update operations across all Fjell libraries.
24
142
  *
@@ -97,25 +215,38 @@ export interface UpdateOptions {
97
215
  */
98
216
  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
217
  /**
100
- * Retrieves all items matching the query.
218
+ * Retrieves all items matching the query with optional pagination.
101
219
  *
102
- * @param query - Optional query to filter items
220
+ * @param query - Optional query to filter items (may include limit/offset for backwards compatibility)
103
221
  * @param locations - Optional location hierarchy to scope the query
104
- * @returns Array of items matching the query
222
+ * @param options - Optional pagination options (takes precedence over query limit/offset)
223
+ * @returns Result containing items and pagination metadata
105
224
  *
106
- * @example
225
+ * @example Get all items
107
226
  * ```typescript
108
- * // Get all users
109
- * const users = await operations.all();
227
+ * const result = await operations.all();
228
+ * // result.items = all items
229
+ * // result.metadata.total = items.length
230
+ * ```
110
231
  *
111
- * // Get users with filter
112
- * const activeUsers = await operations.all({ filter: { status: 'active' } });
232
+ * @example Get paginated items
233
+ * ```typescript
234
+ * const result = await operations.all({}, [], { limit: 50, offset: 0 });
235
+ * // result.items = first 50 items
236
+ * // result.metadata.total = total matching count
237
+ * // result.metadata.hasMore = true if more exist
238
+ * ```
113
239
  *
114
- * // Get items in specific location
115
- * const comments = await operations.all({}, [{kt: 'post', lk: 'post-123'}]);
240
+ * @example Get items in specific location with pagination
241
+ * ```typescript
242
+ * const result = await operations.all(
243
+ * {},
244
+ * [{kt: 'post', lk: 'post-123'}],
245
+ * { limit: 20, offset: 0 }
246
+ * );
116
247
  * ```
117
248
  */
118
- all(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]>;
249
+ all(query?: ItemQuery, locations?: LocKeyArray<L1, L2, L3, L4, L5> | [], options?: AllOptions): Promise<AllOperationResult<V>>;
119
250
  /**
120
251
  * Retrieves the first item matching the query.
121
252
  *
@@ -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, OperationParams, Operations } from "./Operations";
5
5
  /**
6
6
  * Contained Operations interface - specialized for contained (hierarchical) items only.
7
7
  *
@@ -47,7 +47,7 @@ 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
52
  find(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V[]>;
53
53
  findOne(finder: string, params: OperationParams | undefined, locations: LocKeyArray<L1, L2, L3, L4, L5> | []): Promise<V | null>;
@@ -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, 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
@@ -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, OperationParams, Operations } from "./Operations";
5
5
  /**
6
6
  * Primary Operations interface - specialized for primary (top-level) items only.
7
7
  *
@@ -39,7 +39,7 @@ 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
44
  find(finder: string, params?: OperationParams): Promise<V[]>;
45
45
  findOne(finder: string, params?: OperationParams): Promise<V | null>;
@@ -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>, options?: WrapperOptions): 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>;
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.62",
4
+ "version": "4.4.64",
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.58",
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.30",
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",