@luca-financial/luca-schema 2.2.0 → 2.3.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.
@@ -553,7 +553,6 @@ export type Transaction = Common5 & {
553
553
  | 'DISPUTED'
554
554
  | 'REFUNDED'
555
555
  | 'DELETED';
556
- deletedAt?: DeletedAt;
557
556
  };
558
557
  /**
559
558
  * UUID for the item
@@ -623,10 +622,6 @@ export type StatementID = string | null;
623
622
  * Identifier for this transaction in a financial data aggregation service.
624
623
  */
625
624
  export type AggregationServiceID1 = string | null;
626
- /**
627
- * Timestamp when the transaction was soft-deleted, if applicable (UTC).
628
- */
629
- export type DeletedAt = string | null;
630
625
  /**
631
626
  * Defines a split within a transaction.
632
627
  */
@@ -1040,7 +1035,6 @@ export type Transaction = Common & {
1040
1035
  | 'DISPUTED'
1041
1036
  | 'REFUNDED'
1042
1037
  | 'DELETED';
1043
- deletedAt?: DeletedAt;
1044
1038
  };
1045
1039
  /**
1046
1040
  * UUID for the item
@@ -1110,10 +1104,6 @@ export type StatementID = string | null;
1110
1104
  * Identifier for this transaction in a financial data aggregation service.
1111
1105
  */
1112
1106
  export type AggregationServiceID = string | null;
1113
- /**
1114
- * Timestamp when the transaction was soft-deleted, if applicable (UTC).
1115
- */
1116
- export type DeletedAt = string | null;
1117
1107
 
1118
1108
  /**
1119
1109
  * Common properties for all schemas
package/dist/esm/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import * as schemaIndex from './schemas/index.js';
2
2
  import { enums, LucaSchemas } from './enums.js';
3
- import { validate } from './lucaValidator.js';
3
+ import {
4
+ applyDefaults,
5
+ getRequiredFields,
6
+ getValidFields,
7
+ stripInvalidFields,
8
+ validate,
9
+ validateCollection
10
+ } from './lucaValidator.js';
4
11
 
5
12
  const schemas = { ...schemaIndex, enums: schemaIndex.enums };
6
13
 
@@ -14,5 +21,15 @@ export const recurringTransactionEventSchema =
14
21
  export const transactionSchema = schemas.transaction;
15
22
  export const transactionSplitSchema = schemas.transactionSplit;
16
23
 
17
- export { enums, LucaSchemas, schemas, validate };
24
+ export {
25
+ enums,
26
+ LucaSchemas,
27
+ schemas,
28
+ validate,
29
+ validateCollection,
30
+ getValidFields,
31
+ getRequiredFields,
32
+ stripInvalidFields,
33
+ applyDefaults
34
+ };
18
35
  export default schemas;
@@ -25,6 +25,8 @@ const schemas = {
25
25
  const supportSchemas = [commonSchemaJson, enumsSchemaJson];
26
26
 
27
27
  let sharedAjv;
28
+ const validFieldsCache = new Map();
29
+ const requiredFieldsCache = new Map();
28
30
 
29
31
  function getValidator() {
30
32
  if (sharedAjv) return sharedAjv;
@@ -46,14 +48,168 @@ function getValidator() {
46
48
  return sharedAjv;
47
49
  }
48
50
 
49
- export function validate(schemaKey, data) {
50
- const ajv = getValidator();
51
+ function getSchema(schemaKey) {
51
52
  const schema = schemas[schemaKey];
52
53
  if (!schema) {
53
54
  throw new Error(`Unknown schema: ${schemaKey}`);
54
55
  }
56
+ return schema;
57
+ }
58
+
59
+ function usesCommonSchema(schema) {
60
+ if (!Array.isArray(schema?.allOf)) return false;
61
+ return schema.allOf.some(entry => {
62
+ if (!entry || typeof entry !== 'object') return false;
63
+ if (typeof entry.$ref !== 'string') return false;
64
+ return entry.$ref.includes('common.json');
65
+ });
66
+ }
67
+
68
+ function getSchemaProperties(schema) {
69
+ const properties = schema?.properties ?? {};
70
+ if (!usesCommonSchema(schema)) return properties;
71
+ return {
72
+ ...commonSchemaJson.properties,
73
+ ...properties
74
+ };
75
+ }
76
+
77
+ function getSchemaRequired(schema) {
78
+ const required = Array.isArray(schema?.required) ? schema.required : [];
79
+ if (!usesCommonSchema(schema)) return required;
80
+ const commonRequired = Array.isArray(commonSchemaJson.required)
81
+ ? commonSchemaJson.required
82
+ : [];
83
+ return [...commonRequired, ...required];
84
+ }
85
+
86
+ function isPlainObject(value) {
87
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
88
+ const proto = Object.getPrototypeOf(value);
89
+ return proto === Object.prototype || proto === null;
90
+ }
91
+
92
+ function cloneDefault(value) {
93
+ if (typeof structuredClone === 'function') return structuredClone(value);
94
+ if (value && typeof value === 'object') {
95
+ return JSON.parse(JSON.stringify(value));
96
+ }
97
+ return value;
98
+ }
99
+
100
+ export function validate(schemaKey, data) {
101
+ const ajv = getValidator();
102
+ const schema = getSchema(schemaKey);
55
103
  const isValid = ajv.validate(schema, data);
56
104
  return { valid: isValid, errors: ajv.errors ?? [] };
57
105
  }
58
106
 
107
+ /**
108
+ * Returns a cached Set of valid field names for the given schema key.
109
+ * Treat the returned Set as read-only.
110
+ * @param {string} schemaKey
111
+ * @returns {Set<string>}
112
+ */
113
+ export function getValidFields(schemaKey) {
114
+ if (validFieldsCache.has(schemaKey)) {
115
+ return validFieldsCache.get(schemaKey);
116
+ }
117
+ const schema = getSchema(schemaKey);
118
+ const properties = getSchemaProperties(schema);
119
+ const fields = new Set(Object.keys(properties));
120
+ validFieldsCache.set(schemaKey, fields);
121
+ return fields;
122
+ }
123
+
124
+ /**
125
+ * Returns a cached Set of required field names for the given schema key.
126
+ * Treat the returned Set as read-only.
127
+ * @param {string} schemaKey
128
+ * @returns {Set<string>}
129
+ */
130
+ export function getRequiredFields(schemaKey) {
131
+ if (requiredFieldsCache.has(schemaKey)) {
132
+ return requiredFieldsCache.get(schemaKey);
133
+ }
134
+ const schema = getSchema(schemaKey);
135
+ const required = getSchemaRequired(schema);
136
+ const fields = new Set(required);
137
+ requiredFieldsCache.set(schemaKey, fields);
138
+ return fields;
139
+ }
140
+
141
+ /**
142
+ * Returns a new object containing only fields defined in the schema.
143
+ * @param {string} schemaKey
144
+ * @param {object | null | undefined} data
145
+ * @returns {object}
146
+ */
147
+ export function stripInvalidFields(schemaKey, data) {
148
+ if (data === null || data === undefined) return {};
149
+ if (!isPlainObject(data)) {
150
+ throw new TypeError('Expected a plain object for data');
151
+ }
152
+ const validFields = getValidFields(schemaKey);
153
+ const cleaned = {};
154
+ for (const [key, value] of Object.entries(data)) {
155
+ if (validFields.has(key)) cleaned[key] = value;
156
+ }
157
+ return cleaned;
158
+ }
159
+
160
+ /**
161
+ * Returns a new object with top-level schema defaults applied for missing fields.
162
+ * @param {string} schemaKey
163
+ * @param {object | null | undefined} data
164
+ * @returns {object}
165
+ */
166
+ export function applyDefaults(schemaKey, data) {
167
+ if (data === null || data === undefined) return {};
168
+ if (!isPlainObject(data)) {
169
+ throw new TypeError('Expected a plain object for data');
170
+ }
171
+ const schema = getSchema(schemaKey);
172
+ const properties = getSchemaProperties(schema);
173
+ const next = { ...data };
174
+ for (const [key, definition] of Object.entries(properties)) {
175
+ if (next[key] !== undefined) continue;
176
+ if (
177
+ definition &&
178
+ Object.prototype.hasOwnProperty.call(definition, 'default')
179
+ ) {
180
+ next[key] = cloneDefault(definition.default);
181
+ }
182
+ }
183
+ return next;
184
+ }
185
+
186
+ /**
187
+ * Validates an array of entities efficiently and returns structured errors.
188
+ * @param {string} schemaKey
189
+ * @param {Array<any>} arrayOfEntities
190
+ * @returns {{ valid: boolean, errors: Array<{ index: number, entity: any, errors: Array<any> }> }}
191
+ */
192
+ export function validateCollection(schemaKey, arrayOfEntities) {
193
+ if (!Array.isArray(arrayOfEntities)) {
194
+ throw new TypeError('Expected an array of entities');
195
+ }
196
+ const ajv = getValidator();
197
+ const schema = getSchema(schemaKey);
198
+ const validateFn = ajv.getSchema(schema.$id) ?? ajv.compile(schema);
199
+ const errors = [];
200
+
201
+ arrayOfEntities.forEach((entity, index) => {
202
+ const isValid = validateFn(entity);
203
+ if (!isValid) {
204
+ errors.push({
205
+ index,
206
+ entity,
207
+ errors: validateFn.errors ?? []
208
+ });
209
+ }
210
+ });
211
+
212
+ return { valid: errors.length === 0, errors };
213
+ }
214
+
59
215
  export { schemas };
@@ -84,13 +84,6 @@
84
84
  "title": "Transaction State",
85
85
  "$ref": "./enums.json#/$defs/TransactionState",
86
86
  "description": "The current state of the transaction."
87
- },
88
- "deletedAt": {
89
- "type": ["string", "null"],
90
- "title": "Deleted At",
91
- "format": "date-time",
92
- "pattern": "Z$",
93
- "description": "Timestamp when the transaction was soft-deleted, if applicable (UTC)."
94
87
  }
95
88
  },
96
89
  "required": ["accountId", "date", "amount", "description", "transactionState"]
package/dist/index.d.ts CHANGED
@@ -553,7 +553,6 @@ export type Transaction = Common5 & {
553
553
  | 'DISPUTED'
554
554
  | 'REFUNDED'
555
555
  | 'DELETED';
556
- deletedAt?: DeletedAt;
557
556
  };
558
557
  /**
559
558
  * UUID for the item
@@ -623,10 +622,6 @@ export type StatementID = string | null;
623
622
  * Identifier for this transaction in a financial data aggregation service.
624
623
  */
625
624
  export type AggregationServiceID1 = string | null;
626
- /**
627
- * Timestamp when the transaction was soft-deleted, if applicable (UTC).
628
- */
629
- export type DeletedAt = string | null;
630
625
  /**
631
626
  * Defines a split within a transaction.
632
627
  */
@@ -1040,7 +1035,6 @@ export type Transaction = Common & {
1040
1035
  | 'DISPUTED'
1041
1036
  | 'REFUNDED'
1042
1037
  | 'DELETED';
1043
- deletedAt?: DeletedAt;
1044
1038
  };
1045
1039
  /**
1046
1040
  * UUID for the item
@@ -1110,10 +1104,6 @@ export type StatementID = string | null;
1110
1104
  * Identifier for this transaction in a financial data aggregation service.
1111
1105
  */
1112
1106
  export type AggregationServiceID = string | null;
1113
- /**
1114
- * Timestamp when the transaction was soft-deleted, if applicable (UTC).
1115
- */
1116
- export type DeletedAt = string | null;
1117
1107
 
1118
1108
  /**
1119
1109
  * Common properties for all schemas
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luca-financial/luca-schema",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Schemas for the Luca Ledger application",
5
5
  "author": "Johnathan Aspinwall",
6
6
  "main": "dist/esm/index.js",