@stonyx/orm 0.2.5-alpha.0 → 0.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.
- package/README.md +482 -15
- package/config/environment.js +63 -6
- package/dist/aggregates.d.ts +21 -0
- package/dist/aggregates.js +93 -0
- package/dist/attr.d.ts +2 -0
- package/dist/attr.js +22 -0
- package/dist/belongs-to.d.ts +11 -0
- package/dist/belongs-to.js +59 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +148 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +146 -0
- package/dist/db.d.ts +21 -0
- package/dist/db.js +180 -0
- package/dist/exports/db.d.ts +7 -0
- package/{src → dist}/exports/db.js +2 -4
- package/dist/has-many.d.ts +11 -0
- package/dist/has-many.js +58 -0
- package/dist/hooks.d.ts +75 -0
- package/dist/hooks.js +110 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +34 -0
- package/dist/main.d.ts +46 -0
- package/dist/main.js +181 -0
- package/dist/manage-record.d.ts +13 -0
- package/dist/manage-record.js +123 -0
- package/dist/meta-request.d.ts +6 -0
- package/dist/meta-request.js +52 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +57 -0
- package/dist/model-property.d.ts +9 -0
- package/dist/model-property.js +29 -0
- package/dist/model.d.ts +15 -0
- package/dist/model.js +18 -0
- package/dist/mysql/connection.d.ts +14 -0
- package/dist/mysql/connection.js +24 -0
- package/dist/mysql/migration-generator.d.ts +45 -0
- package/dist/mysql/migration-generator.js +254 -0
- package/dist/mysql/migration-runner.d.ts +12 -0
- package/dist/mysql/migration-runner.js +88 -0
- package/dist/mysql/mysql-db.d.ts +100 -0
- package/dist/mysql/mysql-db.js +425 -0
- package/dist/mysql/query-builder.d.ts +10 -0
- package/dist/mysql/query-builder.js +44 -0
- package/dist/mysql/schema-introspector.d.ts +19 -0
- package/dist/mysql/schema-introspector.js +257 -0
- package/dist/mysql/type-map.d.ts +21 -0
- package/dist/mysql/type-map.js +36 -0
- package/dist/orm-request.d.ts +38 -0
- package/dist/orm-request.js +475 -0
- package/dist/plural-registry.d.ts +4 -0
- package/dist/plural-registry.js +9 -0
- package/dist/postgres/connection.d.ts +15 -0
- package/dist/postgres/connection.js +32 -0
- package/dist/postgres/migration-generator.d.ts +45 -0
- package/dist/postgres/migration-generator.js +280 -0
- package/dist/postgres/migration-runner.d.ts +10 -0
- package/dist/postgres/migration-runner.js +87 -0
- package/dist/postgres/postgres-db.d.ts +119 -0
- package/dist/postgres/postgres-db.js +477 -0
- package/dist/postgres/query-builder.d.ts +27 -0
- package/dist/postgres/query-builder.js +98 -0
- package/dist/postgres/schema-introspector.d.ts +29 -0
- package/dist/postgres/schema-introspector.js +296 -0
- package/dist/postgres/type-map.d.ts +23 -0
- package/dist/postgres/type-map.js +56 -0
- package/dist/record.d.ts +75 -0
- package/dist/record.js +129 -0
- package/dist/relationships.d.ts +10 -0
- package/dist/relationships.js +41 -0
- package/dist/schema-helpers.d.ts +20 -0
- package/dist/schema-helpers.js +48 -0
- package/dist/serializer.d.ts +17 -0
- package/dist/serializer.js +136 -0
- package/dist/setup-rest-server.d.ts +1 -0
- package/dist/setup-rest-server.js +52 -0
- package/dist/standalone-db.d.ts +58 -0
- package/dist/standalone-db.js +142 -0
- package/dist/store.d.ts +62 -0
- package/dist/store.js +286 -0
- package/dist/timescale/query-builder.d.ts +43 -0
- package/dist/timescale/query-builder.js +115 -0
- package/dist/timescale/timescale-db.d.ts +45 -0
- package/dist/timescale/timescale-db.js +84 -0
- package/dist/transforms.d.ts +2 -0
- package/dist/transforms.js +17 -0
- package/dist/types/orm-types.d.ts +153 -0
- package/dist/types/orm-types.js +1 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +17 -0
- package/dist/view-resolver.d.ts +8 -0
- package/dist/view-resolver.js +171 -0
- package/dist/view.d.ts +11 -0
- package/dist/view.js +18 -0
- package/package.json +64 -11
- package/src/aggregates.ts +109 -0
- package/src/{attr.js → attr.ts} +2 -2
- package/src/belongs-to.ts +90 -0
- package/src/cli.ts +183 -0
- package/src/commands.ts +179 -0
- package/src/db.ts +232 -0
- package/src/exports/db.ts +7 -0
- package/src/has-many.ts +92 -0
- package/src/hooks.ts +151 -0
- package/src/{index.js → index.ts} +12 -2
- package/src/main.ts +229 -0
- package/src/manage-record.ts +161 -0
- package/src/{meta-request.js → meta-request.ts} +17 -14
- package/src/migrate.ts +72 -0
- package/src/model-property.ts +35 -0
- package/src/model.ts +21 -0
- package/src/mysql/connection.ts +43 -0
- package/src/mysql/migration-generator.ts +337 -0
- package/src/mysql/migration-runner.ts +121 -0
- package/src/mysql/mysql-db.ts +543 -0
- package/src/mysql/query-builder.ts +69 -0
- package/src/mysql/schema-introspector.ts +310 -0
- package/src/mysql/type-map.ts +42 -0
- package/src/orm-request.ts +582 -0
- package/src/plural-registry.ts +12 -0
- package/src/postgres/connection.ts +48 -0
- package/src/postgres/migration-generator.ts +370 -0
- package/src/postgres/migration-runner.ts +115 -0
- package/src/postgres/postgres-db.ts +616 -0
- package/src/postgres/query-builder.ts +148 -0
- package/src/postgres/schema-introspector.ts +360 -0
- package/src/postgres/type-map.ts +61 -0
- package/src/record.ts +186 -0
- package/src/relationships.ts +54 -0
- package/src/schema-helpers.ts +59 -0
- package/src/serializer.ts +161 -0
- package/src/setup-rest-server.ts +62 -0
- package/src/standalone-db.ts +185 -0
- package/src/store.ts +373 -0
- package/src/timescale/query-builder.ts +174 -0
- package/src/timescale/timescale-db.ts +119 -0
- package/src/transforms.ts +20 -0
- package/src/types/mysql2.d.ts +49 -0
- package/src/types/orm-types.ts +158 -0
- package/src/types/pg.d.ts +32 -0
- package/src/types/stonyx-cron.d.ts +5 -0
- package/src/types/stonyx-events.d.ts +4 -0
- package/src/types/stonyx-rest-server.d.ts +16 -0
- package/src/types/stonyx-utils.d.ts +33 -0
- package/src/types/stonyx.d.ts +21 -0
- package/src/utils.ts +22 -0
- package/src/view-resolver.ts +211 -0
- package/src/view.ts +22 -0
- package/.claude/project-structure.md +0 -578
- package/.github/workflows/ci.yml +0 -36
- package/.github/workflows/publish.yml +0 -143
- package/src/belongs-to.js +0 -63
- package/src/db.js +0 -80
- package/src/has-many.js +0 -61
- package/src/main.js +0 -119
- package/src/manage-record.js +0 -103
- package/src/model-property.js +0 -29
- package/src/model.js +0 -9
- package/src/orm-request.js +0 -249
- package/src/record.js +0 -100
- package/src/relationships.js +0 -43
- package/src/serializer.js +0 -138
- package/src/setup-rest-server.js +0 -57
- package/src/store.js +0 -211
- package/src/transforms.js +0 -20
- package/stonyx-bootstrap.cjs +0 -30
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect relationship type and target model from a model property function.
|
|
3
|
+
* Returns null if the property is not a relationship.
|
|
4
|
+
*/
|
|
5
|
+
export function getRelationshipInfo(property) {
|
|
6
|
+
if (typeof property !== 'function')
|
|
7
|
+
return null;
|
|
8
|
+
const relType = property.__relationshipType;
|
|
9
|
+
const modelName = property.__relatedModelName || null;
|
|
10
|
+
if (relType === 'belongsTo')
|
|
11
|
+
return { type: 'belongsTo', modelName };
|
|
12
|
+
if (relType === 'hasMany')
|
|
13
|
+
return { type: 'hasMany', modelName };
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize a model/table name for use in SQL identifiers.
|
|
18
|
+
* Replaces hyphens and slashes with underscores.
|
|
19
|
+
*/
|
|
20
|
+
export function sanitizeTableName(name) {
|
|
21
|
+
return name.replace(/[-/]/g, '_');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sort model schemas in dependency order (belongsTo targets before dependents).
|
|
25
|
+
* Uses depth-first traversal to ensure referenced tables are created first.
|
|
26
|
+
*/
|
|
27
|
+
export function getTopologicalOrder(schemas) {
|
|
28
|
+
const visited = new Set();
|
|
29
|
+
const order = [];
|
|
30
|
+
function visit(name) {
|
|
31
|
+
if (visited.has(name))
|
|
32
|
+
return;
|
|
33
|
+
visited.add(name);
|
|
34
|
+
const schema = schemas[name];
|
|
35
|
+
if (!schema)
|
|
36
|
+
return;
|
|
37
|
+
// Visit dependencies (belongsTo targets) first
|
|
38
|
+
for (const targetModelName of Object.values(schema.relationships.belongsTo)) {
|
|
39
|
+
if (targetModelName)
|
|
40
|
+
visit(targetModelName);
|
|
41
|
+
}
|
|
42
|
+
order.push(name);
|
|
43
|
+
}
|
|
44
|
+
for (const name of Object.keys(schemas)) {
|
|
45
|
+
visit(name);
|
|
46
|
+
}
|
|
47
|
+
return order;
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default class Serializer {
|
|
2
|
+
map: Record<string, unknown>;
|
|
3
|
+
path: string;
|
|
4
|
+
model: Record<string, unknown>;
|
|
5
|
+
constructor(model: Record<string, unknown>);
|
|
6
|
+
/**
|
|
7
|
+
* This method populates the record's instance with instances of
|
|
8
|
+
* the ModelProperty object, while setting parsed values to the record's
|
|
9
|
+
* __data property, which represents the serialized version of the data
|
|
10
|
+
*/
|
|
11
|
+
setProperties(rawData: unknown, record: unknown, options: Record<string, unknown>): void;
|
|
12
|
+
/**
|
|
13
|
+
* OVERRIDE: This hook allows for data manipulation prior to serialization logic
|
|
14
|
+
*/
|
|
15
|
+
normalize(data: unknown): unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare function getComputedProperties(classInstance: Record<string, unknown>): [string, PropertyDescriptor['get']][];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import config from 'stonyx/config';
|
|
2
|
+
import { get, makeArray } from '@stonyx/utils/object';
|
|
3
|
+
const RESERVED_KEYS = ['__name'];
|
|
4
|
+
function isAggregateProperty(v) {
|
|
5
|
+
return typeof v === 'object' && v !== null && v.__kind === 'AggregateProperty';
|
|
6
|
+
}
|
|
7
|
+
function isModelProperty(v) {
|
|
8
|
+
return typeof v === 'object' && v !== null && v.__kind === 'ModelProperty';
|
|
9
|
+
}
|
|
10
|
+
function searchQuery(query, array, key) {
|
|
11
|
+
const result = makeArray(array).find((item) => {
|
|
12
|
+
for (const [prop, value] of Object.entries(query)) {
|
|
13
|
+
if (item[prop] !== value)
|
|
14
|
+
return false;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (!result)
|
|
19
|
+
return null;
|
|
20
|
+
if (key)
|
|
21
|
+
return result[key];
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function query(rawData, pathPrefix, subPath) {
|
|
25
|
+
if (!rawData)
|
|
26
|
+
return null;
|
|
27
|
+
const [path, getter, pointer] = makeArray(subPath);
|
|
28
|
+
const fullPath = `${pathPrefix}${path}`;
|
|
29
|
+
const value = get(rawData, fullPath);
|
|
30
|
+
if (getter === undefined || getter === null)
|
|
31
|
+
return value;
|
|
32
|
+
try {
|
|
33
|
+
switch (typeof getter) {
|
|
34
|
+
case 'object':
|
|
35
|
+
return searchQuery(getter, value, pointer);
|
|
36
|
+
case 'function':
|
|
37
|
+
return getter(value);
|
|
38
|
+
case 'number': {
|
|
39
|
+
const element = value[getter];
|
|
40
|
+
return pointer ? element[pointer] : element;
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
return value[getter];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (config.debug)
|
|
48
|
+
console.error(`Cannot parse value for ${fullPath}.`, { getter, query }, error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export default class Serializer {
|
|
52
|
+
map = {};
|
|
53
|
+
path = '';
|
|
54
|
+
model;
|
|
55
|
+
constructor(model) {
|
|
56
|
+
this.model = model;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* This method populates the record's instance with instances of
|
|
60
|
+
* the ModelProperty object, while setting parsed values to the record's
|
|
61
|
+
* __data property, which represents the serialized version of the data
|
|
62
|
+
*/
|
|
63
|
+
setProperties(rawData, record, options) {
|
|
64
|
+
const { path, model } = this;
|
|
65
|
+
const keys = Object.keys(model).filter(key => !RESERVED_KEYS.includes(key));
|
|
66
|
+
const pathPrefix = path ? `${path}.` : '';
|
|
67
|
+
const rec = record;
|
|
68
|
+
const parsedData = rec.__data;
|
|
69
|
+
const relatedRecords = rec.__relationships;
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const subPath = options.serialize ? (this.map[key] || key) : key;
|
|
72
|
+
const handler = model[key];
|
|
73
|
+
const data = query(rawData, pathPrefix, subPath);
|
|
74
|
+
// Skip fields not present in the update payload (undefined = not provided)
|
|
75
|
+
if (data === undefined && options.update)
|
|
76
|
+
continue;
|
|
77
|
+
// Relationship handling
|
|
78
|
+
if (typeof handler === 'function') {
|
|
79
|
+
// Pass relationship key name to handler for pending fulfillment
|
|
80
|
+
const handlerOptions = { ...options, _relationshipKey: key };
|
|
81
|
+
const childRecord = handler(record, data, handlerOptions);
|
|
82
|
+
rec[key] = childRecord;
|
|
83
|
+
relatedRecords[key] = childRecord;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Aggregate property handling — use the rawData value, not the aggregate descriptor
|
|
87
|
+
if (isAggregateProperty(handler)) {
|
|
88
|
+
parsedData[key] = data;
|
|
89
|
+
rec[key] = data;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// Direct assignment handling
|
|
93
|
+
if (!isModelProperty(handler)) {
|
|
94
|
+
parsedData[key] = handler;
|
|
95
|
+
rec[key] = handler;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const prop = handler;
|
|
99
|
+
Object.defineProperty(record, key, {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
get: () => prop.value,
|
|
103
|
+
set(newValue) {
|
|
104
|
+
prop.ignoreFirstTransform = !options.transform;
|
|
105
|
+
prop.value = newValue;
|
|
106
|
+
parsedData[key] = prop.value;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
rec[key] = data;
|
|
110
|
+
}
|
|
111
|
+
if (options.update)
|
|
112
|
+
return;
|
|
113
|
+
// Serialize computed properties
|
|
114
|
+
for (const [key, getter] of getComputedProperties(this.model)) {
|
|
115
|
+
Object.defineProperty(record, key, {
|
|
116
|
+
enumerable: true,
|
|
117
|
+
get: () => getter.call(record)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
rec.__serialized = true;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* OVERRIDE: This hook allows for data manipulation prior to serialization logic
|
|
124
|
+
*/
|
|
125
|
+
normalize(data) {
|
|
126
|
+
return data;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function getComputedProperties(classInstance) {
|
|
130
|
+
const proto = Object.getPrototypeOf(classInstance);
|
|
131
|
+
if (!proto || proto === Object.prototype)
|
|
132
|
+
return [];
|
|
133
|
+
return Object.entries(Object.getOwnPropertyDescriptors(proto))
|
|
134
|
+
.filter(([key, descriptor]) => key !== 'constructor' && descriptor.get)
|
|
135
|
+
.map(([key, descriptor]) => [key, descriptor.get]);
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function (route: string, accessPath: string, metaRoute: boolean): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { waitForModule } from 'stonyx';
|
|
2
|
+
import { store } from '@stonyx/orm';
|
|
3
|
+
import OrmRequest from './orm-request.js';
|
|
4
|
+
import MetaRequest from './meta-request.js';
|
|
5
|
+
import RestServer from '@stonyx/rest-server';
|
|
6
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
7
|
+
import { dbKey } from './db.js';
|
|
8
|
+
import { getPluralName } from './plural-registry.js';
|
|
9
|
+
import log from 'stonyx/log';
|
|
10
|
+
export default async function (route, accessPath, metaRoute) {
|
|
11
|
+
const accessFiles = {};
|
|
12
|
+
try {
|
|
13
|
+
await forEachFileImport(accessPath, (accessClass) => {
|
|
14
|
+
const accessInstance = new accessClass();
|
|
15
|
+
const { models } = accessInstance;
|
|
16
|
+
if (!models)
|
|
17
|
+
throw new Error(`Access class "${accessClass.name}" must define a "models" list`);
|
|
18
|
+
if (models.length === 0)
|
|
19
|
+
return; // No models to assign access to
|
|
20
|
+
if (typeof accessInstance.access !== 'function')
|
|
21
|
+
throw new Error(`Access class "${accessClass.name}" must declare an "access" method`);
|
|
22
|
+
const availableModels = Array.from(store.data.keys());
|
|
23
|
+
for (const model of models === '*' ? availableModels : models) {
|
|
24
|
+
if (model === dbKey)
|
|
25
|
+
continue;
|
|
26
|
+
if (!store.data.has(model))
|
|
27
|
+
throw new Error(`Unable to define access for Invalid Model "${model}". Model does not exist`);
|
|
28
|
+
if (accessFiles[model])
|
|
29
|
+
throw new Error(`Access for model "${model}" has already been defined by another access class.`);
|
|
30
|
+
accessFiles[model] = accessInstance.access;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
log.error?.(error instanceof Error ? error.message : String(error));
|
|
36
|
+
log.warn?.('You must define a valid access configuration file in order to access ORM generated REST endpoints.');
|
|
37
|
+
}
|
|
38
|
+
await waitForModule('rest-server');
|
|
39
|
+
// Remove "/" prefix and name mount point accordingly
|
|
40
|
+
const name = route === '/' ? 'index' : (route[0] === '/' ? route.slice(1) : route);
|
|
41
|
+
// Configure endpoints for models and views with access configuration
|
|
42
|
+
for (const [model, access] of Object.entries(accessFiles)) {
|
|
43
|
+
const pluralizedModel = getPluralName(model);
|
|
44
|
+
const modelName = name === 'index' ? pluralizedModel : `${name}/${pluralizedModel}`;
|
|
45
|
+
RestServer.instance.mountRoute(OrmRequest, { name: modelName, options: { model, access } });
|
|
46
|
+
}
|
|
47
|
+
// Mount the meta route when metaRoute config is enabled
|
|
48
|
+
if (metaRoute) {
|
|
49
|
+
log.warn?.('SECURITY RISK! - Meta route is enabled via metaRoute config. This feature is intended for development purposes only!');
|
|
50
|
+
RestServer.instance.mountRoute(MetaRequest, { name });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
interface StandaloneDBOptions {
|
|
9
|
+
dbPath?: string;
|
|
10
|
+
mode?: 'file' | 'directory';
|
|
11
|
+
directory?: string;
|
|
12
|
+
}
|
|
13
|
+
interface DBRecord {
|
|
14
|
+
id: string | number;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
export default class StandaloneDB {
|
|
18
|
+
readonly mode: 'file' | 'directory';
|
|
19
|
+
readonly dbPath: string;
|
|
20
|
+
readonly directory: string;
|
|
21
|
+
constructor(options?: StandaloneDBOptions);
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the directory path for directory mode.
|
|
24
|
+
*/
|
|
25
|
+
getDirPath(): string;
|
|
26
|
+
/**
|
|
27
|
+
* List available collections by inspecting either the db.json keys
|
|
28
|
+
* or the files in the db directory.
|
|
29
|
+
*/
|
|
30
|
+
getCollections(): Promise<string[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Read all records for a collection.
|
|
33
|
+
*/
|
|
34
|
+
readCollection(collection: string): Promise<DBRecord[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Write all records for a collection.
|
|
37
|
+
*/
|
|
38
|
+
writeCollection(collection: string, records: DBRecord[]): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Get a single record by id.
|
|
41
|
+
*/
|
|
42
|
+
get(collection: string, id: string | number): Promise<DBRecord | null>;
|
|
43
|
+
/**
|
|
44
|
+
* List all records in a collection.
|
|
45
|
+
*/
|
|
46
|
+
list(collection: string): Promise<DBRecord[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Create a new record. Auto-assigns an integer id if none provided.
|
|
49
|
+
*/
|
|
50
|
+
create(collection: string, data: DBRecord): Promise<DBRecord>;
|
|
51
|
+
/**
|
|
52
|
+
* Delete a record by id.
|
|
53
|
+
*/
|
|
54
|
+
delete(collection: string, id: string | number): Promise<DBRecord>;
|
|
55
|
+
private _readJSON;
|
|
56
|
+
private _writeJSON;
|
|
57
|
+
}
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
export default class StandaloneDB {
|
|
11
|
+
mode;
|
|
12
|
+
dbPath;
|
|
13
|
+
directory;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.mode = options.mode || 'directory';
|
|
16
|
+
this.dbPath = options.dbPath || 'db.json';
|
|
17
|
+
this.directory = options.directory || 'db';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the directory path for directory mode.
|
|
21
|
+
*/
|
|
22
|
+
getDirPath() {
|
|
23
|
+
const dbDir = path.dirname(path.resolve(this.dbPath));
|
|
24
|
+
return path.join(dbDir, this.directory);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* List available collections by inspecting either the db.json keys
|
|
28
|
+
* or the files in the db directory.
|
|
29
|
+
*/
|
|
30
|
+
async getCollections() {
|
|
31
|
+
if (this.mode === 'directory') {
|
|
32
|
+
const dirPath = this.getDirPath();
|
|
33
|
+
try {
|
|
34
|
+
const files = await fs.readdir(dirPath);
|
|
35
|
+
return files
|
|
36
|
+
.filter(f => f.endsWith('.json'))
|
|
37
|
+
.map(f => f.replace('.json', ''));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// File mode -- read db.json and return its top-level keys
|
|
44
|
+
try {
|
|
45
|
+
const data = await this._readJSON(this.dbPath);
|
|
46
|
+
return Object.keys(data).filter(key => Array.isArray(data[key]));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read all records for a collection.
|
|
54
|
+
*/
|
|
55
|
+
async readCollection(collection) {
|
|
56
|
+
if (this.mode === 'directory') {
|
|
57
|
+
const filePath = path.join(this.getDirPath(), `${collection}.json`);
|
|
58
|
+
return this._readJSON(filePath);
|
|
59
|
+
}
|
|
60
|
+
const data = await this._readJSON(this.dbPath);
|
|
61
|
+
return data[collection] || [];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Write all records for a collection.
|
|
65
|
+
*/
|
|
66
|
+
async writeCollection(collection, records) {
|
|
67
|
+
if (this.mode === 'directory') {
|
|
68
|
+
const dirPath = this.getDirPath();
|
|
69
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
70
|
+
const filePath = path.join(dirPath, `${collection}.json`);
|
|
71
|
+
await this._writeJSON(filePath, records);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// File mode -- read full db, update collection, write back
|
|
75
|
+
let data;
|
|
76
|
+
try {
|
|
77
|
+
data = await this._readJSON(this.dbPath);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
data = {};
|
|
81
|
+
}
|
|
82
|
+
data[collection] = records;
|
|
83
|
+
await this._writeJSON(this.dbPath, data);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get a single record by id.
|
|
87
|
+
*/
|
|
88
|
+
async get(collection, id) {
|
|
89
|
+
const records = await this.readCollection(collection);
|
|
90
|
+
const numericId = Number(id);
|
|
91
|
+
return records.find(r => r.id === id || r.id === numericId) || null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* List all records in a collection.
|
|
95
|
+
*/
|
|
96
|
+
async list(collection) {
|
|
97
|
+
return this.readCollection(collection);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create a new record. Auto-assigns an integer id if none provided.
|
|
101
|
+
*/
|
|
102
|
+
async create(collection, data) {
|
|
103
|
+
const records = await this.readCollection(collection);
|
|
104
|
+
if (!data.id) {
|
|
105
|
+
const maxId = records.reduce((max, r) => {
|
|
106
|
+
const rid = typeof r.id === 'number' ? r.id : 0;
|
|
107
|
+
return rid > max ? rid : max;
|
|
108
|
+
}, 0);
|
|
109
|
+
data.id = maxId + 1;
|
|
110
|
+
}
|
|
111
|
+
// Check for duplicate id
|
|
112
|
+
const existing = records.find(r => r.id === data.id);
|
|
113
|
+
if (existing) {
|
|
114
|
+
throw new Error(`Record with id ${data.id} already exists in '${collection}'`);
|
|
115
|
+
}
|
|
116
|
+
records.push(data);
|
|
117
|
+
await this.writeCollection(collection, records);
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Delete a record by id.
|
|
122
|
+
*/
|
|
123
|
+
async delete(collection, id) {
|
|
124
|
+
const records = await this.readCollection(collection);
|
|
125
|
+
const numericId = Number(id);
|
|
126
|
+
const index = records.findIndex(r => r.id === id || r.id === numericId);
|
|
127
|
+
if (index === -1) {
|
|
128
|
+
throw new Error(`Record with id '${id}' not found in '${collection}'`);
|
|
129
|
+
}
|
|
130
|
+
const [removed] = records.splice(index, 1);
|
|
131
|
+
await this.writeCollection(collection, records);
|
|
132
|
+
return removed;
|
|
133
|
+
}
|
|
134
|
+
// -- Private helpers --
|
|
135
|
+
async _readJSON(filePath) {
|
|
136
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
137
|
+
return JSON.parse(content);
|
|
138
|
+
}
|
|
139
|
+
async _writeJSON(filePath, data) {
|
|
140
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
interface UnloadOptions {
|
|
2
|
+
includeChildren?: boolean;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export default class Store {
|
|
6
|
+
static instance: Store | undefined;
|
|
7
|
+
data: Map<string, Map<number | string, unknown>>;
|
|
8
|
+
/**
|
|
9
|
+
* Set by Orm during init — resolves memory flag for a model name.
|
|
10
|
+
*/
|
|
11
|
+
_memoryResolver: ((modelName: string) => boolean) | null;
|
|
12
|
+
/**
|
|
13
|
+
* Set by Orm during init — reference to the SQL adapter instance for on-demand queries.
|
|
14
|
+
*/
|
|
15
|
+
_sqlDb: {
|
|
16
|
+
findRecord(modelName: string, id: unknown): Promise<unknown>;
|
|
17
|
+
findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]>;
|
|
18
|
+
} | null;
|
|
19
|
+
constructor();
|
|
20
|
+
/**
|
|
21
|
+
* Synchronous memory-only access.
|
|
22
|
+
* Returns the record if it exists in the in-memory store, undefined otherwise.
|
|
23
|
+
* Does NOT query the database. For memory:false models, use find() instead.
|
|
24
|
+
*/
|
|
25
|
+
get(key: string): Map<number | string, unknown> | undefined;
|
|
26
|
+
get(key: string, id: number | string): unknown;
|
|
27
|
+
/**
|
|
28
|
+
* Async authoritative read. Always queries the SQL database for memory: false models.
|
|
29
|
+
* For memory: true models, returns from store (already loaded on boot).
|
|
30
|
+
*/
|
|
31
|
+
find(modelName: string, id: number | string): Promise<unknown>;
|
|
32
|
+
/**
|
|
33
|
+
* Async read for all records of a model. Always queries MySQL for memory: false models.
|
|
34
|
+
* For memory: true models, returns from store.
|
|
35
|
+
*/
|
|
36
|
+
findAll(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Async query — always hits MySQL, never reads from memory cache.
|
|
39
|
+
* Use for complex queries, aggregations, or when you need guaranteed freshness.
|
|
40
|
+
*/
|
|
41
|
+
query(modelName: string, conditions?: Record<string, unknown>): Promise<unknown[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Check if a model is configured for in-memory storage.
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
private _isMemoryModel;
|
|
47
|
+
set(key: string, value: Map<number | string, unknown>): void;
|
|
48
|
+
remove(key: string, id?: number | string): void;
|
|
49
|
+
unloadRecord(model: string, id: unknown, options?: UnloadOptions): void;
|
|
50
|
+
unloadAllRecords(model: string, options?: UnloadOptions): void;
|
|
51
|
+
private _removeFromHasManyArrays;
|
|
52
|
+
private _nullifyBelongsToReferences;
|
|
53
|
+
private _cleanupRelationshipRegistries;
|
|
54
|
+
/**
|
|
55
|
+
* Extracts hasMany and non-bidirectional belongsTo children from a record
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
private _getChildren;
|
|
59
|
+
private _isBidirectionalRelationship;
|
|
60
|
+
private _buildUnloadQueue;
|
|
61
|
+
}
|
|
62
|
+
export {};
|