@luca-financial/luca-schema 2.2.0 → 2.3.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.
@@ -405,7 +405,7 @@ export type IsLocked = boolean;
405
405
  */
406
406
  export type RecurringTransaction = Common3 & {
407
407
  accountId: AccountID1;
408
- categoryId?: CategoryID;
408
+ categoryId: CategoryID;
409
409
  amount: Amount;
410
410
  description: Description1;
411
411
  /**
@@ -534,8 +534,6 @@ export type Transaction = Common5 & {
534
534
  currency?: Currency;
535
535
  amount: Amount1;
536
536
  description: Description2;
537
- memo?: Memo;
538
- counterparty?: Counterparty;
539
537
  categoryId?: CategoryID1;
540
538
  statementId?: StatementID;
541
539
  aggregationServiceId?: AggregationServiceID1;
@@ -553,7 +551,6 @@ export type Transaction = Common5 & {
553
551
  | 'DISPUTED'
554
552
  | 'REFUNDED'
555
553
  | 'DELETED';
556
- deletedAt?: DeletedAt;
557
554
  };
558
555
  /**
559
556
  * UUID for the item
@@ -603,14 +600,6 @@ export type Amount1 = number;
603
600
  * Description of the transaction
604
601
  */
605
602
  export type Description2 = string;
606
- /**
607
- * Additional notes for the transaction.
608
- */
609
- export type Memo = string | null;
610
- /**
611
- * Name of the other party (merchant/payor/payee).
612
- */
613
- export type Counterparty = string | null;
614
603
  /**
615
604
  * Category UUID for this transaction
616
605
  */
@@ -623,10 +612,6 @@ export type StatementID = string | null;
623
612
  * Identifier for this transaction in a financial data aggregation service.
624
613
  */
625
614
  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
615
  /**
631
616
  * Defines a split within a transaction.
632
617
  */
@@ -635,7 +620,6 @@ export type TransactionSplit = Common6 & {
635
620
  amount: Amount2;
636
621
  categoryId: CategoryID2;
637
622
  description?: Description3;
638
- memo?: Memo1;
639
623
  };
640
624
  /**
641
625
  * UUID for the item
@@ -673,10 +657,6 @@ export type CategoryID2 = string | null;
673
657
  * Optional description for the split.
674
658
  */
675
659
  export type Description3 = string | null;
676
- /**
677
- * Additional notes for this split.
678
- */
679
- export type Memo1 = string | null;
680
660
 
681
661
  /**
682
662
  * Schema for the luca ledger
@@ -791,7 +771,7 @@ export interface Common6 {
791
771
  */
792
772
  export type RecurringTransaction = Common & {
793
773
  accountId: AccountID;
794
- categoryId?: CategoryID;
774
+ categoryId: CategoryID;
795
775
  amount: Amount;
796
776
  description: Description;
797
777
  /**
@@ -1021,8 +1001,6 @@ export type Transaction = Common & {
1021
1001
  currency?: Currency;
1022
1002
  amount: Amount;
1023
1003
  description: Description;
1024
- memo?: Memo;
1025
- counterparty?: Counterparty;
1026
1004
  categoryId?: CategoryID;
1027
1005
  statementId?: StatementID;
1028
1006
  aggregationServiceId?: AggregationServiceID;
@@ -1040,7 +1018,6 @@ export type Transaction = Common & {
1040
1018
  | 'DISPUTED'
1041
1019
  | 'REFUNDED'
1042
1020
  | 'DELETED';
1043
- deletedAt?: DeletedAt;
1044
1021
  };
1045
1022
  /**
1046
1023
  * UUID for the item
@@ -1090,14 +1067,6 @@ export type Amount = number;
1090
1067
  * Description of the transaction
1091
1068
  */
1092
1069
  export type Description = string;
1093
- /**
1094
- * Additional notes for the transaction.
1095
- */
1096
- export type Memo = string | null;
1097
- /**
1098
- * Name of the other party (merchant/payor/payee).
1099
- */
1100
- export type Counterparty = string | null;
1101
1070
  /**
1102
1071
  * Category UUID for this transaction
1103
1072
  */
@@ -1110,10 +1079,6 @@ export type StatementID = string | null;
1110
1079
  * Identifier for this transaction in a financial data aggregation service.
1111
1080
  */
1112
1081
  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
1082
 
1118
1083
  /**
1119
1084
  * Common properties for all schemas
@@ -1134,7 +1099,6 @@ export type TransactionSplit = Common & {
1134
1099
  amount: Amount;
1135
1100
  categoryId: CategoryID;
1136
1101
  description?: Description;
1137
- memo?: Memo;
1138
1102
  };
1139
1103
  /**
1140
1104
  * UUID for the item
@@ -1172,10 +1136,6 @@ export type CategoryID = string | null;
1172
1136
  * Optional description for the split.
1173
1137
  */
1174
1138
  export type Description = string | null;
1175
- /**
1176
- * Additional notes for this split.
1177
- */
1178
- export type Memo = string | null;
1179
1139
 
1180
1140
  /**
1181
1141
  * Common properties for all schemas
package/dist/esm/index.js CHANGED
@@ -1,6 +1,12 @@
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
+ getRequiredFields,
5
+ getValidFields,
6
+ stripInvalidFields,
7
+ validate,
8
+ validateCollection
9
+ } from './lucaValidator.js';
4
10
 
5
11
  const schemas = { ...schemaIndex, enums: schemaIndex.enums };
6
12
 
@@ -14,5 +20,14 @@ export const recurringTransactionEventSchema =
14
20
  export const transactionSchema = schemas.transaction;
15
21
  export const transactionSplitSchema = schemas.transactionSplit;
16
22
 
17
- export { enums, LucaSchemas, schemas, validate };
23
+ export {
24
+ enums,
25
+ LucaSchemas,
26
+ schemas,
27
+ validate,
28
+ validateCollection,
29
+ getValidFields,
30
+ getRequiredFields,
31
+ stripInvalidFields
32
+ };
18
33
  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,134 @@ 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
+ export function validate(schemaKey, data) {
93
+ const ajv = getValidator();
94
+ const schema = getSchema(schemaKey);
55
95
  const isValid = ajv.validate(schema, data);
56
96
  return { valid: isValid, errors: ajv.errors ?? [] };
57
97
  }
58
98
 
99
+ /**
100
+ * Returns a cached Set of valid field names for the given schema key.
101
+ * Treat the returned Set as read-only.
102
+ * @param {string} schemaKey
103
+ * @returns {Set<string>}
104
+ */
105
+ export function getValidFields(schemaKey) {
106
+ if (validFieldsCache.has(schemaKey)) {
107
+ return validFieldsCache.get(schemaKey);
108
+ }
109
+ const schema = getSchema(schemaKey);
110
+ const properties = getSchemaProperties(schema);
111
+ const fields = new Set(Object.keys(properties));
112
+ validFieldsCache.set(schemaKey, fields);
113
+ return fields;
114
+ }
115
+
116
+ /**
117
+ * Returns a cached Set of required field names for the given schema key.
118
+ * Treat the returned Set as read-only.
119
+ * @param {string} schemaKey
120
+ * @returns {Set<string>}
121
+ */
122
+ export function getRequiredFields(schemaKey) {
123
+ if (requiredFieldsCache.has(schemaKey)) {
124
+ return requiredFieldsCache.get(schemaKey);
125
+ }
126
+ const schema = getSchema(schemaKey);
127
+ const required = getSchemaRequired(schema);
128
+ const fields = new Set(required);
129
+ requiredFieldsCache.set(schemaKey, fields);
130
+ return fields;
131
+ }
132
+
133
+ /**
134
+ * Returns a new object containing only fields defined in the schema.
135
+ * @param {string} schemaKey
136
+ * @param {object | null | undefined} data
137
+ * @returns {object}
138
+ */
139
+ export function stripInvalidFields(schemaKey, data) {
140
+ if (data === null || data === undefined) return {};
141
+ if (!isPlainObject(data)) {
142
+ throw new TypeError('Expected a plain object for data');
143
+ }
144
+ const validFields = getValidFields(schemaKey);
145
+ const cleaned = {};
146
+ for (const [key, value] of Object.entries(data)) {
147
+ if (validFields.has(key)) cleaned[key] = value;
148
+ }
149
+ return cleaned;
150
+ }
151
+
152
+ /**
153
+ * Validates an array of entities efficiently and returns structured errors.
154
+ * @param {string} schemaKey
155
+ * @param {Array<any>} arrayOfEntities
156
+ * @returns {{ valid: boolean, errors: Array<{ index: number, entity: any, errors: Array<any> }> }}
157
+ */
158
+ export function validateCollection(schemaKey, arrayOfEntities) {
159
+ if (!Array.isArray(arrayOfEntities)) {
160
+ throw new TypeError('Expected an array of entities');
161
+ }
162
+ const ajv = getValidator();
163
+ const schema = getSchema(schemaKey);
164
+ const validateFn = ajv.getSchema(schema.$id) ?? ajv.compile(schema);
165
+ const errors = [];
166
+
167
+ arrayOfEntities.forEach((entity, index) => {
168
+ const isValid = validateFn(entity);
169
+ if (!isValid) {
170
+ errors.push({
171
+ index,
172
+ entity,
173
+ errors: validateFn.errors ?? []
174
+ });
175
+ }
176
+ });
177
+
178
+ return { valid: errors.length === 0, errors };
179
+ }
180
+
59
181
  export { schemas };
@@ -19,6 +19,7 @@
19
19
  "type": "string",
20
20
  "title": "Account Type",
21
21
  "$ref": "./enums.json#/$defs/AccountType",
22
+ "default": "SAVINGS",
22
23
  "description": "The type of the account."
23
24
  },
24
25
  "institution": {
@@ -30,6 +30,7 @@
30
30
  "type": ["string", "null"],
31
31
  "title": "Parent Category ID",
32
32
  "format": "uuid",
33
+ "default": null,
33
34
  "description": "The identifier of the parent category, if any. Null if the category is top-level. Parents must be top-level (no deeper than two levels; enforced outside JSON Schema)."
34
35
  }
35
36
  },
@@ -20,6 +20,7 @@
20
20
  "type": ["string", "null"],
21
21
  "title": "Category ID",
22
22
  "format": "uuid",
23
+ "default": null,
23
24
  "description": "Category identifier for organizing the transaction. Can be null if not categorized."
24
25
  },
25
26
  "amount": {
@@ -36,18 +37,21 @@
36
37
  "type": "string",
37
38
  "title": "Transaction Frequency",
38
39
  "$ref": "./enums.json#/$defs/RecurringTransactionFrequency",
40
+ "default": "MONTH",
39
41
  "description": "Defines the base unit of time for the repetition."
40
42
  },
41
43
  "interval": {
42
44
  "type": "integer",
43
45
  "title": "Frequency Interval",
44
46
  "minimum": 1,
47
+ "default": 1,
45
48
  "description": "Specifies the number of frequency units between each occurrence (e.g., every 2 weeks)."
46
49
  },
47
50
  "occurrences": {
48
51
  "type": ["integer", "null"],
49
52
  "title": "Total Occurrences",
50
53
  "minimum": 1,
54
+ "default": null,
51
55
  "description": "The total number of times the transaction should occur. Can be null if not specified."
52
56
  },
53
57
  "startOn": {
@@ -66,11 +70,13 @@
66
70
  "type": "string",
67
71
  "title": "Recurring Transaction State",
68
72
  "$ref": "./enums.json#/$defs/RecurringTransactionState",
73
+ "default": "ACTIVE",
69
74
  "description": "Current state of the recurring transaction series."
70
75
  }
71
76
  },
72
77
  "required": [
73
78
  "accountId",
79
+ "categoryId",
74
80
  "amount",
75
81
  "description",
76
82
  "frequency",
@@ -52,20 +52,11 @@
52
52
  "minLength": 1,
53
53
  "description": "Description of the transaction"
54
54
  },
55
- "memo": {
56
- "type": ["string", "null"],
57
- "title": "Memo",
58
- "description": "Additional notes for the transaction."
59
- },
60
- "counterparty": {
61
- "type": ["string", "null"],
62
- "title": "Counterparty",
63
- "description": "Name of the other party (merchant/payor/payee)."
64
- },
65
55
  "categoryId": {
66
56
  "type": ["string", "null"],
67
57
  "title": "Category ID",
68
58
  "format": "uuid",
59
+ "default": null,
69
60
  "description": "Category UUID for this transaction"
70
61
  },
71
62
  "statementId": {
@@ -83,14 +74,8 @@
83
74
  "type": "string",
84
75
  "title": "Transaction State",
85
76
  "$ref": "./enums.json#/$defs/TransactionState",
77
+ "default": "PLANNED",
86
78
  "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
79
  }
95
80
  },
96
81
  "required": ["accountId", "date", "amount", "description", "transactionState"]
@@ -26,17 +26,13 @@
26
26
  "type": ["string", "null"],
27
27
  "title": "Category ID",
28
28
  "format": "uuid",
29
+ "default": null,
29
30
  "description": "The identifier of the category for this split."
30
31
  },
31
32
  "description": {
32
33
  "type": ["string", "null"],
33
34
  "title": "Description",
34
35
  "description": "Optional description for the split."
35
- },
36
- "memo": {
37
- "type": ["string", "null"],
38
- "title": "Memo",
39
- "description": "Additional notes for this split."
40
36
  }
41
37
  },
42
38
  "required": ["transactionId", "amount", "categoryId"]
package/dist/index.d.ts CHANGED
@@ -405,7 +405,7 @@ export type IsLocked = boolean;
405
405
  */
406
406
  export type RecurringTransaction = Common3 & {
407
407
  accountId: AccountID1;
408
- categoryId?: CategoryID;
408
+ categoryId: CategoryID;
409
409
  amount: Amount;
410
410
  description: Description1;
411
411
  /**
@@ -534,8 +534,6 @@ export type Transaction = Common5 & {
534
534
  currency?: Currency;
535
535
  amount: Amount1;
536
536
  description: Description2;
537
- memo?: Memo;
538
- counterparty?: Counterparty;
539
537
  categoryId?: CategoryID1;
540
538
  statementId?: StatementID;
541
539
  aggregationServiceId?: AggregationServiceID1;
@@ -553,7 +551,6 @@ export type Transaction = Common5 & {
553
551
  | 'DISPUTED'
554
552
  | 'REFUNDED'
555
553
  | 'DELETED';
556
- deletedAt?: DeletedAt;
557
554
  };
558
555
  /**
559
556
  * UUID for the item
@@ -603,14 +600,6 @@ export type Amount1 = number;
603
600
  * Description of the transaction
604
601
  */
605
602
  export type Description2 = string;
606
- /**
607
- * Additional notes for the transaction.
608
- */
609
- export type Memo = string | null;
610
- /**
611
- * Name of the other party (merchant/payor/payee).
612
- */
613
- export type Counterparty = string | null;
614
603
  /**
615
604
  * Category UUID for this transaction
616
605
  */
@@ -623,10 +612,6 @@ export type StatementID = string | null;
623
612
  * Identifier for this transaction in a financial data aggregation service.
624
613
  */
625
614
  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
615
  /**
631
616
  * Defines a split within a transaction.
632
617
  */
@@ -635,7 +620,6 @@ export type TransactionSplit = Common6 & {
635
620
  amount: Amount2;
636
621
  categoryId: CategoryID2;
637
622
  description?: Description3;
638
- memo?: Memo1;
639
623
  };
640
624
  /**
641
625
  * UUID for the item
@@ -673,10 +657,6 @@ export type CategoryID2 = string | null;
673
657
  * Optional description for the split.
674
658
  */
675
659
  export type Description3 = string | null;
676
- /**
677
- * Additional notes for this split.
678
- */
679
- export type Memo1 = string | null;
680
660
 
681
661
  /**
682
662
  * Schema for the luca ledger
@@ -791,7 +771,7 @@ export interface Common6 {
791
771
  */
792
772
  export type RecurringTransaction = Common & {
793
773
  accountId: AccountID;
794
- categoryId?: CategoryID;
774
+ categoryId: CategoryID;
795
775
  amount: Amount;
796
776
  description: Description;
797
777
  /**
@@ -1021,8 +1001,6 @@ export type Transaction = Common & {
1021
1001
  currency?: Currency;
1022
1002
  amount: Amount;
1023
1003
  description: Description;
1024
- memo?: Memo;
1025
- counterparty?: Counterparty;
1026
1004
  categoryId?: CategoryID;
1027
1005
  statementId?: StatementID;
1028
1006
  aggregationServiceId?: AggregationServiceID;
@@ -1040,7 +1018,6 @@ export type Transaction = Common & {
1040
1018
  | 'DISPUTED'
1041
1019
  | 'REFUNDED'
1042
1020
  | 'DELETED';
1043
- deletedAt?: DeletedAt;
1044
1021
  };
1045
1022
  /**
1046
1023
  * UUID for the item
@@ -1090,14 +1067,6 @@ export type Amount = number;
1090
1067
  * Description of the transaction
1091
1068
  */
1092
1069
  export type Description = string;
1093
- /**
1094
- * Additional notes for the transaction.
1095
- */
1096
- export type Memo = string | null;
1097
- /**
1098
- * Name of the other party (merchant/payor/payee).
1099
- */
1100
- export type Counterparty = string | null;
1101
1070
  /**
1102
1071
  * Category UUID for this transaction
1103
1072
  */
@@ -1110,10 +1079,6 @@ export type StatementID = string | null;
1110
1079
  * Identifier for this transaction in a financial data aggregation service.
1111
1080
  */
1112
1081
  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
1082
 
1118
1083
  /**
1119
1084
  * Common properties for all schemas
@@ -1134,7 +1099,6 @@ export type TransactionSplit = Common & {
1134
1099
  amount: Amount;
1135
1100
  categoryId: CategoryID;
1136
1101
  description?: Description;
1137
- memo?: Memo;
1138
1102
  };
1139
1103
  /**
1140
1104
  * UUID for the item
@@ -1172,10 +1136,6 @@ export type CategoryID = string | null;
1172
1136
  * Optional description for the split.
1173
1137
  */
1174
1138
  export type Description = string | null;
1175
- /**
1176
- * Additional notes for this split.
1177
- */
1178
- export type Memo = string | null;
1179
1139
 
1180
1140
  /**
1181
1141
  * 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.1",
4
4
  "description": "Schemas for the Luca Ledger application",
5
5
  "author": "Johnathan Aspinwall",
6
6
  "main": "dist/esm/index.js",