@stonyx/orm 0.2.1-beta.9 → 0.2.1-beta.90

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.
Files changed (171) hide show
  1. package/README.md +64 -6
  2. package/config/environment.js +37 -1
  3. package/dist/aggregates.d.ts +21 -0
  4. package/dist/aggregates.js +93 -0
  5. package/dist/attr.d.ts +2 -0
  6. package/dist/attr.js +22 -0
  7. package/dist/belongs-to.d.ts +11 -0
  8. package/dist/belongs-to.js +59 -0
  9. package/dist/cli.d.ts +22 -0
  10. package/dist/cli.js +148 -0
  11. package/dist/commands.d.ts +7 -0
  12. package/dist/commands.js +146 -0
  13. package/dist/db.d.ts +21 -0
  14. package/dist/db.js +180 -0
  15. package/dist/exports/db.d.ts +7 -0
  16. package/{src → dist}/exports/db.js +2 -4
  17. package/dist/has-many.d.ts +11 -0
  18. package/dist/has-many.js +58 -0
  19. package/dist/hooks.d.ts +62 -0
  20. package/dist/hooks.js +110 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +34 -0
  23. package/dist/main.d.ts +46 -0
  24. package/dist/main.js +181 -0
  25. package/dist/manage-record.d.ts +13 -0
  26. package/dist/manage-record.js +123 -0
  27. package/dist/meta-request.d.ts +6 -0
  28. package/dist/meta-request.js +52 -0
  29. package/dist/migrate.d.ts +2 -0
  30. package/dist/migrate.js +57 -0
  31. package/dist/model-property.d.ts +9 -0
  32. package/dist/model-property.js +29 -0
  33. package/dist/model.d.ts +15 -0
  34. package/dist/model.js +18 -0
  35. package/dist/mysql/connection.d.ts +14 -0
  36. package/dist/mysql/connection.js +24 -0
  37. package/dist/mysql/migration-generator.d.ts +45 -0
  38. package/dist/mysql/migration-generator.js +254 -0
  39. package/dist/mysql/migration-runner.d.ts +12 -0
  40. package/dist/mysql/migration-runner.js +88 -0
  41. package/dist/mysql/mysql-db.d.ts +100 -0
  42. package/dist/mysql/mysql-db.js +425 -0
  43. package/dist/mysql/query-builder.d.ts +10 -0
  44. package/dist/mysql/query-builder.js +44 -0
  45. package/dist/mysql/schema-introspector.d.ts +19 -0
  46. package/dist/mysql/schema-introspector.js +291 -0
  47. package/dist/mysql/type-map.d.ts +21 -0
  48. package/dist/mysql/type-map.js +36 -0
  49. package/dist/orm-request.d.ts +38 -0
  50. package/dist/orm-request.js +474 -0
  51. package/dist/plural-registry.d.ts +4 -0
  52. package/dist/plural-registry.js +9 -0
  53. package/dist/postgres/connection.d.ts +15 -0
  54. package/dist/postgres/connection.js +32 -0
  55. package/dist/postgres/migration-generator.d.ts +45 -0
  56. package/dist/postgres/migration-generator.js +261 -0
  57. package/dist/postgres/migration-runner.d.ts +10 -0
  58. package/dist/postgres/migration-runner.js +87 -0
  59. package/dist/postgres/postgres-db.d.ts +119 -0
  60. package/dist/postgres/postgres-db.js +477 -0
  61. package/dist/postgres/query-builder.d.ts +27 -0
  62. package/dist/postgres/query-builder.js +98 -0
  63. package/dist/postgres/schema-introspector.d.ts +29 -0
  64. package/dist/postgres/schema-introspector.js +314 -0
  65. package/dist/postgres/type-map.d.ts +23 -0
  66. package/dist/postgres/type-map.js +56 -0
  67. package/dist/record.d.ts +75 -0
  68. package/dist/record.js +129 -0
  69. package/dist/relationships.d.ts +10 -0
  70. package/dist/relationships.js +41 -0
  71. package/dist/serializer.d.ts +17 -0
  72. package/dist/serializer.js +136 -0
  73. package/dist/setup-rest-server.d.ts +1 -0
  74. package/dist/setup-rest-server.js +52 -0
  75. package/dist/standalone-db.d.ts +58 -0
  76. package/dist/standalone-db.js +142 -0
  77. package/dist/store.d.ts +62 -0
  78. package/dist/store.js +286 -0
  79. package/dist/timescale/query-builder.d.ts +43 -0
  80. package/dist/timescale/query-builder.js +115 -0
  81. package/dist/timescale/timescale-db.d.ts +45 -0
  82. package/dist/timescale/timescale-db.js +84 -0
  83. package/dist/transforms.d.ts +2 -0
  84. package/dist/transforms.js +17 -0
  85. package/dist/types/orm-types.d.ts +142 -0
  86. package/dist/types/orm-types.js +1 -0
  87. package/dist/utils.d.ts +7 -0
  88. package/dist/utils.js +17 -0
  89. package/dist/view-resolver.d.ts +8 -0
  90. package/dist/view-resolver.js +171 -0
  91. package/dist/view.d.ts +11 -0
  92. package/dist/view.js +18 -0
  93. package/package.json +57 -15
  94. package/src/aggregates.ts +109 -0
  95. package/src/{attr.js → attr.ts} +2 -2
  96. package/src/belongs-to.ts +90 -0
  97. package/src/cli.ts +183 -0
  98. package/src/{commands.js → commands.ts} +179 -170
  99. package/src/{db.js → db.ts} +55 -29
  100. package/src/exports/db.ts +7 -0
  101. package/src/has-many.ts +92 -0
  102. package/src/{hooks.js → hooks.ts} +41 -27
  103. package/src/{index.js → index.ts} +11 -2
  104. package/src/main.ts +229 -0
  105. package/src/manage-record.ts +161 -0
  106. package/src/{meta-request.js → meta-request.ts} +17 -14
  107. package/src/{migrate.js → migrate.ts} +9 -9
  108. package/src/model-property.ts +35 -0
  109. package/src/model.ts +21 -0
  110. package/src/mysql/{connection.js → connection.ts} +43 -28
  111. package/src/mysql/migration-generator.ts +337 -0
  112. package/src/mysql/{migration-runner.js → migration-runner.ts} +121 -110
  113. package/src/mysql/mysql-db.ts +543 -0
  114. package/src/mysql/{query-builder.js → query-builder.ts} +69 -64
  115. package/src/mysql/schema-introspector.ts +358 -0
  116. package/src/mysql/{type-map.js → type-map.ts} +42 -37
  117. package/src/{orm-request.js → orm-request.ts} +186 -108
  118. package/src/plural-registry.ts +12 -0
  119. package/src/postgres/connection.ts +48 -0
  120. package/src/postgres/migration-generator.ts +348 -0
  121. package/src/postgres/migration-runner.ts +115 -0
  122. package/src/postgres/postgres-db.ts +616 -0
  123. package/src/postgres/query-builder.ts +148 -0
  124. package/src/postgres/schema-introspector.ts +386 -0
  125. package/src/postgres/type-map.ts +61 -0
  126. package/src/record.ts +186 -0
  127. package/src/relationships.ts +54 -0
  128. package/src/serializer.ts +161 -0
  129. package/src/{setup-rest-server.js → setup-rest-server.ts} +18 -16
  130. package/src/standalone-db.ts +185 -0
  131. package/src/store.ts +373 -0
  132. package/src/timescale/query-builder.ts +174 -0
  133. package/src/timescale/timescale-db.ts +119 -0
  134. package/src/transforms.ts +20 -0
  135. package/src/types/mysql2.d.ts +49 -0
  136. package/src/types/orm-types.ts +146 -0
  137. package/src/types/pg.d.ts +32 -0
  138. package/src/types/stonyx-cron.d.ts +5 -0
  139. package/src/types/stonyx-events.d.ts +4 -0
  140. package/src/types/stonyx-rest-server.d.ts +16 -0
  141. package/src/types/stonyx-utils.d.ts +33 -0
  142. package/src/types/stonyx.d.ts +21 -0
  143. package/src/utils.ts +22 -0
  144. package/src/view-resolver.ts +211 -0
  145. package/src/view.ts +22 -0
  146. package/.claude/code-style-rules.md +0 -44
  147. package/.claude/hooks.md +0 -250
  148. package/.claude/index.md +0 -279
  149. package/.claude/usage-patterns.md +0 -217
  150. package/.github/workflows/ci.yml +0 -16
  151. package/.github/workflows/publish.yml +0 -51
  152. package/improvements.md +0 -139
  153. package/project-structure.md +0 -343
  154. package/src/belongs-to.js +0 -63
  155. package/src/has-many.js +0 -61
  156. package/src/main.js +0 -148
  157. package/src/manage-record.js +0 -118
  158. package/src/model-property.js +0 -29
  159. package/src/model.js +0 -9
  160. package/src/mysql/migration-generator.js +0 -188
  161. package/src/mysql/mysql-db.js +0 -320
  162. package/src/mysql/schema-introspector.js +0 -158
  163. package/src/record.js +0 -127
  164. package/src/relationships.js +0 -43
  165. package/src/serializer.js +0 -138
  166. package/src/store.js +0 -211
  167. package/src/transforms.js +0 -20
  168. package/src/utils.js +0 -12
  169. package/test-events-setup.js +0 -41
  170. package/test-hooks-manual.js +0 -54
  171. package/test-hooks-with-logging.js +0 -52
package/src/record.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { store } from '@stonyx/orm';
2
+ import { getComputedProperties } from "./serializer.js";
3
+ import { camelCaseToKebabCase } from '@stonyx/utils/string';
4
+ import { getPluralName } from './plural-registry.js';
5
+ import type Serializer from './serializer.js';
6
+
7
+ interface ToJSONOptions {
8
+ fields?: Set<string>;
9
+ baseUrl?: string;
10
+ }
11
+
12
+ interface SerializeOptions {
13
+ update?: boolean;
14
+ serialize?: boolean;
15
+ transform?: boolean;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ interface UnloadOptions {
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ interface RelationshipLinks {
24
+ self: string;
25
+ related: string;
26
+ }
27
+
28
+ interface RelationshipEntry {
29
+ data: { type: string; id: unknown } | { type: string; id: unknown }[] | null;
30
+ links?: RelationshipLinks;
31
+ }
32
+
33
+ interface JSONAPIResult {
34
+ attributes: { [key: string]: unknown };
35
+ relationships: { [key: string]: RelationshipEntry };
36
+ id: unknown;
37
+ type: string;
38
+ links?: { self: string };
39
+ }
40
+
41
+ export default class Record {
42
+ /** @private */
43
+ __data: { [key: string]: unknown } = {};
44
+ /** @private */
45
+ __relationships: { [key: string]: unknown } = {};
46
+ /** @private */
47
+ __serialized = false;
48
+ /** @private */
49
+ __model: { __name: string; [key: string]: unknown };
50
+ /** @private */
51
+ __serializer: Serializer;
52
+
53
+ [key: string]: unknown;
54
+
55
+ constructor(model: { __name: string; [key: string]: unknown }, serializer: Serializer) {
56
+ this.__model = model;
57
+ this.__serializer = serializer;
58
+ }
59
+
60
+ serialize(rawData?: unknown, options: SerializeOptions = {}): { [key: string]: unknown } {
61
+ const { __data: data } = this;
62
+
63
+ if (this.__serialized && !options.update) {
64
+ const relatedIds: { [key: string]: unknown } = {};
65
+
66
+ for (const [key, childRecord] of Object.entries(this.__relationships)) {
67
+ relatedIds[key] = Array.isArray(childRecord)
68
+ ? childRecord.map((r: Record) => r.id)
69
+ : (childRecord as Record)?.id ?? null;
70
+ }
71
+
72
+ return { ...data, ...relatedIds };
73
+ }
74
+
75
+ const normalizedData = this.__serializer.normalize(rawData);
76
+ this.__serializer.setProperties(normalizedData, this, options);
77
+
78
+ return data;
79
+ }
80
+
81
+ // Similar to serialize, but preserves top level relationship records
82
+ format(): { [key: string]: unknown } {
83
+ if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
84
+
85
+ const { __data: data } = this;
86
+ const records: { [key: string]: unknown } = {};
87
+
88
+ for (const [key, childRecord] of Object.entries(this.__relationships)) {
89
+ if (Array.isArray(childRecord)) {
90
+ // Deduplicate by record ID — keep last occurrence (latest data wins)
91
+ const seen = new Set<unknown>();
92
+ const unique: Record[] = [];
93
+
94
+ for (let i = childRecord.length - 1; i >= 0; i--) {
95
+ const r = childRecord[i] as Record;
96
+ if (!seen.has(r.id)) {
97
+ seen.add(r.id);
98
+ unique.push(r);
99
+ }
100
+ }
101
+
102
+ unique.reverse();
103
+ records[key] = unique.map((r: Record) => r.serialize());
104
+ } else {
105
+ records[key] = (childRecord as Record)?.serialize() ?? null;
106
+ }
107
+ }
108
+
109
+ return { ...data, ...records };
110
+ }
111
+
112
+ // Formats record for JSON API output
113
+ toJSON(options: ToJSONOptions = {}): JSONAPIResult {
114
+ if (!this.__serialized) throw new Error('Record must be serialized before being converted to JSON');
115
+
116
+ const { fields, baseUrl } = options;
117
+ const { __data: data } = this;
118
+ const modelName = this.__model.__name;
119
+ const pluralizedModelName = getPluralName(modelName);
120
+ const recordId = data.id;
121
+ const relationships: { [key: string]: RelationshipEntry } = {};
122
+ const attributes: { [key: string]: unknown } = {};
123
+
124
+ for (const [key, value] of Object.entries(data)) {
125
+ if (key === 'id') continue;
126
+ if (fields && !fields.has(key)) continue;
127
+ attributes[key] = value;
128
+ }
129
+
130
+ for (const [key, getter] of getComputedProperties(this.__model)) {
131
+ if (fields && !fields.has(key)) continue;
132
+ attributes[key] = (getter as () => unknown).call(this);
133
+ }
134
+
135
+ for (const [key, childRecord] of Object.entries(this.__relationships)) {
136
+ if (fields && !fields.has(key)) continue;
137
+
138
+ const relationshipData = Array.isArray(childRecord)
139
+ ? childRecord.map((r: Record) => ({ type: r.__model.__name, id: r.id }))
140
+ : childRecord ? { type: (childRecord as Record).__model.__name, id: (childRecord as Record).id } : null;
141
+
142
+ // Dasherize the key for URL paths (e.g., accessLinks -> access-links)
143
+ const dasherizedKey = camelCaseToKebabCase(key);
144
+
145
+ relationships[dasherizedKey] = { data: relationshipData };
146
+
147
+ // Add links to relationship if baseUrl provided
148
+ if (baseUrl) {
149
+ relationships[dasherizedKey].links = {
150
+ self: `${baseUrl}/${pluralizedModelName}/${recordId}/relationships/${dasherizedKey}`,
151
+ related: `${baseUrl}/${pluralizedModelName}/${recordId}/${dasherizedKey}`
152
+ };
153
+ }
154
+ }
155
+
156
+ const result: JSONAPIResult = {
157
+ attributes,
158
+ relationships,
159
+ id: recordId,
160
+ type: modelName,
161
+ };
162
+
163
+ // Add resource links if baseUrl provided
164
+ if (baseUrl) {
165
+ result.links = {
166
+ self: `${baseUrl}/${pluralizedModelName}/${recordId}`
167
+ };
168
+ }
169
+
170
+ return result;
171
+ }
172
+
173
+ unload(options: UnloadOptions = {}): void {
174
+ store.unloadRecord(this.__model.__name, this.id, options);
175
+ }
176
+
177
+ clean(): void {
178
+ try {
179
+ for (const key of Object.keys(this)) {
180
+ delete this[key];
181
+ }
182
+ } catch {
183
+ // Ignore errors during cleanup, as some keys may not be deletable
184
+ }
185
+ }
186
+ }
@@ -0,0 +1,54 @@
1
+ import { relationships } from '@stonyx/orm';
2
+ import type { HasManyMap, BelongsToMap, GlobalMap, PendingMap, PendingBelongsToMap } from './types/orm-types.js';
3
+
4
+ // TODO: Refactor mapping to remove a level of iteration
5
+ export function getRelationships(type: string, sourceModel: string, targetModel: string, relationshipId?: string): Map<unknown, unknown> | undefined {
6
+ let allRelationships = relationships.get(type) as Map<string, Map<string, Map<unknown, unknown>>> | undefined;
7
+
8
+ if (!allRelationships) {
9
+ allRelationships = new Map();
10
+ relationships.set(type, allRelationships);
11
+ }
12
+
13
+ // create relationship map for this type of it doesn't already exist
14
+ if (!allRelationships.has(sourceModel)) allRelationships.set(sourceModel, new Map());
15
+
16
+ const modelRelationship = allRelationships.get(sourceModel) as Map<string, Map<unknown, unknown>> | undefined;
17
+ if (!modelRelationship) return undefined;
18
+
19
+ if (!modelRelationship.has(targetModel)) modelRelationship.set(targetModel, new Map());
20
+
21
+ const relationship = modelRelationship.get(targetModel) as Map<unknown, unknown> | undefined;
22
+
23
+ // TODO: Determine whether already having id should be handled differently
24
+ //if (relationship.has(relationshipId)) return;
25
+
26
+ return relationship;
27
+ }
28
+
29
+ export function getHasManyRelationships(sourceModel: string, targetModel: string): Map<unknown, unknown> | undefined {
30
+ return (relationships.get('hasMany') as HasManyMap | undefined)?.get(sourceModel)?.get(targetModel);
31
+ }
32
+
33
+ /** Typed accessors for the relationship registry */
34
+ export function getHasManyRegistry(): HasManyMap {
35
+ return relationships.get('hasMany') as HasManyMap;
36
+ }
37
+
38
+ export function getBelongsToRegistry(): BelongsToMap {
39
+ return relationships.get('belongsTo') as BelongsToMap;
40
+ }
41
+
42
+ export function getGlobalRegistry(): GlobalMap {
43
+ return relationships.get('global') as GlobalMap;
44
+ }
45
+
46
+ export function getPendingRegistry(): PendingMap {
47
+ return relationships.get('pending') as PendingMap;
48
+ }
49
+
50
+ export function getPendingBelongsToRegistry(): PendingBelongsToMap {
51
+ return relationships.get('pendingBelongsTo') as PendingBelongsToMap;
52
+ }
53
+
54
+ export const TYPES: string[] = ['global', 'hasMany', 'belongsTo', 'pending'];
@@ -0,0 +1,161 @@
1
+ import config from 'stonyx/config';
2
+ import { get, makeArray } from '@stonyx/utils/object';
3
+ import type { AggregateProperty } from './aggregates.js';
4
+ import type ModelProperty from './model-property.js';
5
+
6
+ const RESERVED_KEYS = ['__name'];
7
+
8
+ function isAggregateProperty(v: unknown): v is AggregateProperty {
9
+ return typeof v === 'object' && v !== null && (v as { __kind?: string }).__kind === 'AggregateProperty';
10
+ }
11
+
12
+ function isModelProperty(v: unknown): v is ModelProperty {
13
+ return typeof v === 'object' && v !== null && (v as { __kind?: string }).__kind === 'ModelProperty';
14
+ }
15
+
16
+ function searchQuery(query: Record<string, unknown>, array: unknown, key?: string): unknown {
17
+ const result = makeArray(array).find((item: unknown) => {
18
+ for (const [prop, value] of Object.entries(query)) {
19
+ if ((item as Record<string, unknown>)[prop] !== value) return false;
20
+
21
+ return true;
22
+ }
23
+ });
24
+
25
+ if (!result) return null;
26
+ if (key) return (result as Record<string, unknown>)[key];
27
+
28
+ return result;
29
+ }
30
+
31
+ function query(rawData: unknown, pathPrefix: string, subPath: unknown): unknown {
32
+ if (!rawData) return null;
33
+
34
+ const [path, getter, pointer] = makeArray(subPath) as [string, unknown, string | undefined];
35
+ const fullPath = `${pathPrefix}${path}`;
36
+ const value = get(rawData as Record<string, unknown>, fullPath);
37
+
38
+ if (getter === undefined || getter === null) return value;
39
+
40
+ try {
41
+ switch (typeof getter) {
42
+ case 'object':
43
+ return searchQuery(getter as Record<string, unknown>, value, pointer);
44
+
45
+ case 'function':
46
+ return (getter as (v: unknown) => unknown)(value);
47
+
48
+ case 'number': {
49
+ const element = (value as unknown[])[getter];
50
+ return pointer ? (element as Record<string, unknown>)[pointer] : element;
51
+ }
52
+
53
+ default:
54
+ return (value as Record<string, unknown>)[getter as string];
55
+ }
56
+ } catch (error) {
57
+ if (config.debug) console.error(`Cannot parse value for ${fullPath}.`, { getter, query }, error);
58
+ }
59
+ }
60
+
61
+ export default class Serializer {
62
+ map: Record<string, unknown> = {};
63
+ path = '';
64
+ model: Record<string, unknown>;
65
+
66
+ constructor(model: Record<string, unknown>) {
67
+ this.model = model;
68
+ }
69
+
70
+ /**
71
+ * This method populates the record's instance with instances of
72
+ * the ModelProperty object, while setting parsed values to the record's
73
+ * __data property, which represents the serialized version of the data
74
+ */
75
+ setProperties(rawData: unknown, record: unknown, options: Record<string, unknown>): void {
76
+ const { path, model } = this;
77
+ const keys = Object.keys(model).filter(key => !RESERVED_KEYS.includes(key));
78
+ const pathPrefix = path ? `${path}.` : '';
79
+ const rec = record as Record<string, unknown>;
80
+ const parsedData = rec.__data as Record<string, unknown>;
81
+ const relatedRecords = rec.__relationships as Record<string, unknown>;
82
+
83
+ for (const key of keys) {
84
+ const subPath = options.serialize ? ((this.map as Record<string, unknown>)[key] || key) : key;
85
+ const handler = model[key];
86
+ const data = query(rawData, pathPrefix, subPath);
87
+
88
+ // Skip fields not present in the update payload (undefined = not provided)
89
+ if (data === undefined && options.update) continue;
90
+
91
+ // Relationship handling
92
+ if (typeof handler === 'function') {
93
+ // Pass relationship key name to handler for pending fulfillment
94
+ const handlerOptions = { ...options, _relationshipKey: key };
95
+ const childRecord = handler(record, data, handlerOptions);
96
+
97
+ rec[key] = childRecord;
98
+ relatedRecords[key] = childRecord;
99
+
100
+ continue;
101
+ }
102
+
103
+ // Aggregate property handling — use the rawData value, not the aggregate descriptor
104
+ if (isAggregateProperty(handler)) {
105
+ parsedData[key] = data;
106
+ rec[key] = data;
107
+ continue;
108
+ }
109
+
110
+ // Direct assignment handling
111
+ if (!isModelProperty(handler)) {
112
+ parsedData[key] = handler;
113
+ rec[key] = handler;
114
+ continue;
115
+ }
116
+
117
+ const prop = handler as { value: unknown; ignoreFirstTransform: boolean };
118
+
119
+ Object.defineProperty(record, key, {
120
+ enumerable: true,
121
+ configurable: true,
122
+ get: () => prop.value,
123
+ set(newValue: unknown) {
124
+ prop.ignoreFirstTransform = !options.transform;
125
+ prop.value = newValue;
126
+ parsedData[key] = prop.value;
127
+ }
128
+ });
129
+
130
+ rec[key] = data;
131
+ }
132
+
133
+ if (options.update) return;
134
+
135
+ // Serialize computed properties
136
+ for (const [key, getter] of getComputedProperties(this.model)) {
137
+ Object.defineProperty(record, key, {
138
+ enumerable: true,
139
+ get: () => (getter as () => unknown).call(record)
140
+ });
141
+ }
142
+
143
+ rec.__serialized = true;
144
+ }
145
+
146
+ /**
147
+ * OVERRIDE: This hook allows for data manipulation prior to serialization logic
148
+ */
149
+ normalize(data: unknown): unknown {
150
+ return data;
151
+ }
152
+ }
153
+
154
+ export function getComputedProperties(classInstance: Record<string, unknown>): [string, PropertyDescriptor['get']][] {
155
+ const proto = Object.getPrototypeOf(classInstance);
156
+ if (!proto || proto === Object.prototype) return [];
157
+
158
+ return Object.entries(Object.getOwnPropertyDescriptors(proto))
159
+ .filter(([key, descriptor]) => key !== 'constructor' && descriptor.get)
160
+ .map(([key, descriptor]) => [key, descriptor.get]);
161
+ }
@@ -5,21 +5,26 @@ import MetaRequest from './meta-request.js';
5
5
  import RestServer from '@stonyx/rest-server';
6
6
  import { forEachFileImport } from '@stonyx/utils/file';
7
7
  import { dbKey } from './db.js';
8
- import { pluralize } from './utils.js';
8
+ import { getPluralName } from './plural-registry.js';
9
9
  import log from 'stonyx/log';
10
10
 
11
- export default async function(route, accessPath, metaRoute) {
12
- let accessFiles = {};
13
-
11
+ interface AccessInstance {
12
+ models: string[] | '*';
13
+ access: (request: unknown) => unknown;
14
+ }
15
+
16
+ export default async function(route: string, accessPath: string, metaRoute: boolean): Promise<void> {
17
+ const accessFiles: Record<string, (request: unknown) => unknown> = {};
18
+
14
19
  try {
15
- await forEachFileImport(accessPath, accessClass => {
16
- const accessInstance = new accessClass();
20
+ await forEachFileImport(accessPath, (accessClass: unknown) => {
21
+ const accessInstance = new (accessClass as new () => AccessInstance)();
17
22
  const { models } = accessInstance;
18
23
 
19
- if (!models) throw new Error(`Access class "${accessClass.name}" must define a "models" list`);
24
+ if (!models) throw new Error(`Access class "${(accessClass as { name: string }).name}" must define a "models" list`);
20
25
 
21
26
  if (models.length === 0) return; // No models to assign access to
22
- if (typeof accessInstance.access !== 'function') throw new Error(`Access class "${accessClass.name}" must declare an "access" method`);
27
+ if (typeof accessInstance.access !== 'function') throw new Error(`Access class "${(accessClass as { name: string }).name}" must declare an "access" method`);
23
28
 
24
29
  const availableModels = Array.from(store.data.keys());
25
30
 
@@ -32,8 +37,8 @@ export default async function(route, accessPath, metaRoute) {
32
37
  }
33
38
  });
34
39
  } catch (error) {
35
- log.error(error.message);
36
- log.warn('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
40
+ log.error?.(error instanceof Error ? error.message : String(error));
41
+ log.warn?.('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
37
42
  }
38
43
 
39
44
  await waitForModule('rest-server');
@@ -41,20 +46,17 @@ export default async function(route, accessPath, metaRoute) {
41
46
  // Remove "/" prefix and name mount point accordingly
42
47
  const name = route === '/' ? 'index' : (route[0] === '/' ? route.slice(1) : route);
43
48
 
44
- // Configure endpoints for models with access configuration
49
+ // Configure endpoints for models and views with access configuration
45
50
  for (const [model, access] of Object.entries(accessFiles)) {
46
- const pluralizedModel = pluralize(model);
51
+ const pluralizedModel = getPluralName(model);
47
52
  const modelName = name === 'index' ? pluralizedModel : `${name}/${pluralizedModel}`;
48
53
  RestServer.instance.mountRoute(OrmRequest, { name: modelName, options: { model, access } });
49
54
  }
50
55
 
51
56
  // Mount the meta route when metaRoute config is enabled
52
57
  if (metaRoute) {
53
- log.warn('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
58
+ log.warn?.('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
54
59
 
55
60
  RestServer.instance.mountRoute(MetaRequest, { name });
56
61
  }
57
-
58
- // Cleanup references
59
- accessFiles = null;
60
62
  }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Standalone JSON database layer for CLI usage.
3
+ *
4
+ * Reads and writes directly to JSON files without requiring the Stonyx
5
+ * bootstrap, ORM init, or any framework dependencies. Supports both
6
+ * single-file and directory modes.
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+
12
+ interface StandaloneDBOptions {
13
+ dbPath?: string;
14
+ mode?: 'file' | 'directory';
15
+ directory?: string;
16
+ }
17
+
18
+ interface DBRecord {
19
+ id: string | number;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ export default class StandaloneDB {
24
+ readonly mode: 'file' | 'directory';
25
+ readonly dbPath: string;
26
+ readonly directory: string;
27
+
28
+ constructor(options: StandaloneDBOptions = {}) {
29
+ this.mode = options.mode || 'directory';
30
+ this.dbPath = options.dbPath || 'db.json';
31
+ this.directory = options.directory || 'db';
32
+ }
33
+
34
+ /**
35
+ * Resolve the directory path for directory mode.
36
+ */
37
+ getDirPath(): string {
38
+ const dbDir = path.dirname(path.resolve(this.dbPath));
39
+ return path.join(dbDir, this.directory);
40
+ }
41
+
42
+ /**
43
+ * List available collections by inspecting either the db.json keys
44
+ * or the files in the db directory.
45
+ */
46
+ async getCollections(): Promise<string[]> {
47
+ if (this.mode === 'directory') {
48
+ const dirPath = this.getDirPath();
49
+
50
+ try {
51
+ const files = await fs.readdir(dirPath);
52
+ return files
53
+ .filter(f => f.endsWith('.json'))
54
+ .map(f => f.replace('.json', ''));
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ // File mode -- read db.json and return its top-level keys
61
+ try {
62
+ const data = await this._readJSON(this.dbPath) as Record<string, unknown>;
63
+ return Object.keys(data).filter(key => Array.isArray(data[key]));
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Read all records for a collection.
71
+ */
72
+ async readCollection(collection: string): Promise<DBRecord[]> {
73
+ if (this.mode === 'directory') {
74
+ const filePath = path.join(this.getDirPath(), `${collection}.json`);
75
+ return this._readJSON(filePath) as Promise<DBRecord[]>;
76
+ }
77
+
78
+ const data = await this._readJSON(this.dbPath) as Record<string, DBRecord[]>;
79
+ return data[collection] || [];
80
+ }
81
+
82
+ /**
83
+ * Write all records for a collection.
84
+ */
85
+ async writeCollection(collection: string, records: DBRecord[]): Promise<void> {
86
+ if (this.mode === 'directory') {
87
+ const dirPath = this.getDirPath();
88
+ await fs.mkdir(dirPath, { recursive: true });
89
+
90
+ const filePath = path.join(dirPath, `${collection}.json`);
91
+ await this._writeJSON(filePath, records);
92
+ return;
93
+ }
94
+
95
+ // File mode -- read full db, update collection, write back
96
+ let data: Record<string, unknown>;
97
+
98
+ try {
99
+ data = await this._readJSON(this.dbPath) as Record<string, unknown>;
100
+ } catch {
101
+ data = {};
102
+ }
103
+
104
+ data[collection] = records;
105
+ await this._writeJSON(this.dbPath, data);
106
+ }
107
+
108
+ /**
109
+ * Get a single record by id.
110
+ */
111
+ async get(collection: string, id: string | number): Promise<DBRecord | null> {
112
+ const records = await this.readCollection(collection);
113
+ const numericId = Number(id);
114
+
115
+ return records.find(r =>
116
+ r.id === id || r.id === numericId
117
+ ) || null;
118
+ }
119
+
120
+ /**
121
+ * List all records in a collection.
122
+ */
123
+ async list(collection: string): Promise<DBRecord[]> {
124
+ return this.readCollection(collection);
125
+ }
126
+
127
+ /**
128
+ * Create a new record. Auto-assigns an integer id if none provided.
129
+ */
130
+ async create(collection: string, data: DBRecord): Promise<DBRecord> {
131
+ const records = await this.readCollection(collection);
132
+
133
+ if (!data.id) {
134
+ const maxId = records.reduce((max, r) => {
135
+ const rid = typeof r.id === 'number' ? r.id : 0;
136
+ return rid > max ? rid : max;
137
+ }, 0);
138
+
139
+ data.id = maxId + 1;
140
+ }
141
+
142
+ // Check for duplicate id
143
+ const existing = records.find(r => r.id === data.id);
144
+ if (existing) {
145
+ throw new Error(`Record with id ${data.id} already exists in '${collection}'`);
146
+ }
147
+
148
+ records.push(data);
149
+ await this.writeCollection(collection, records);
150
+
151
+ return data;
152
+ }
153
+
154
+ /**
155
+ * Delete a record by id.
156
+ */
157
+ async delete(collection: string, id: string | number): Promise<DBRecord> {
158
+ const records = await this.readCollection(collection);
159
+ const numericId = Number(id);
160
+
161
+ const index = records.findIndex(r =>
162
+ r.id === id || r.id === numericId
163
+ );
164
+
165
+ if (index === -1) {
166
+ throw new Error(`Record with id '${id}' not found in '${collection}'`);
167
+ }
168
+
169
+ const [removed] = records.splice(index, 1);
170
+ await this.writeCollection(collection, records);
171
+
172
+ return removed;
173
+ }
174
+
175
+ // -- Private helpers --
176
+
177
+ private async _readJSON(filePath: string): Promise<unknown> {
178
+ const content = await fs.readFile(filePath, 'utf-8');
179
+ return JSON.parse(content);
180
+ }
181
+
182
+ private async _writeJSON(filePath: string, data: unknown): Promise<void> {
183
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
184
+ }
185
+ }