@stonyx/orm 0.2.1-alpha.3 → 0.2.1-alpha.31
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 +465 -15
- package/config/environment.js +29 -6
- package/package.json +33 -8
- package/src/aggregates.js +93 -0
- package/src/belongs-to.js +4 -1
- package/src/cli.js +177 -0
- package/src/commands.js +170 -0
- package/src/db.js +142 -6
- package/src/has-many.js +4 -1
- package/src/hooks.js +124 -0
- package/src/index.js +12 -2
- package/src/main.js +77 -4
- package/src/manage-record.js +30 -4
- package/src/migrate.js +72 -0
- package/src/model-property.js +2 -2
- package/src/model.js +11 -0
- package/src/mysql/connection.js +28 -0
- package/src/mysql/migration-generator.js +286 -0
- package/src/mysql/migration-runner.js +110 -0
- package/src/mysql/mysql-db.js +473 -0
- package/src/mysql/query-builder.js +64 -0
- package/src/mysql/schema-introspector.js +325 -0
- package/src/mysql/type-map.js +37 -0
- package/src/orm-request.js +308 -59
- package/src/plural-registry.js +12 -0
- package/src/record.js +35 -8
- package/src/serializer.js +9 -2
- package/src/setup-rest-server.js +5 -2
- package/src/standalone-db.js +176 -0
- package/src/store.js +130 -1
- package/src/utils.js +12 -0
- package/src/view-resolver.js +183 -0
- package/src/view.js +21 -0
- package/.claude/project-structure.md +0 -578
- package/.github/workflows/ci.yml +0 -36
- package/.github/workflows/publish.yml +0 -143
- package/stonyx-bootstrap.cjs +0 -30
package/src/hooks.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Stone Costa
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Middleware-based hooks registry for ORM operations.
|
|
19
|
+
* Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Map of "operation:model" -> handler[]
|
|
23
|
+
const beforeHooks = new Map();
|
|
24
|
+
const afterHooks = new Map();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register a before hook middleware that runs before the operation executes.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
|
|
30
|
+
* @param {string} model - Model name (e.g., 'user', 'animal')
|
|
31
|
+
* @param {Function} handler - Middleware function (context) => any
|
|
32
|
+
* - Return undefined to continue to next hook/handler
|
|
33
|
+
* - Return any value to halt operation (integer = HTTP status, object = response body)
|
|
34
|
+
* @returns {Function} Unsubscribe function
|
|
35
|
+
*/
|
|
36
|
+
export function beforeHook(operation, model, handler) {
|
|
37
|
+
const key = `${operation}:${model}`;
|
|
38
|
+
if (!beforeHooks.has(key)) {
|
|
39
|
+
beforeHooks.set(key, []);
|
|
40
|
+
}
|
|
41
|
+
beforeHooks.get(key).push(handler);
|
|
42
|
+
|
|
43
|
+
// Return unsubscribe function
|
|
44
|
+
return () => {
|
|
45
|
+
const hooks = beforeHooks.get(key);
|
|
46
|
+
if (hooks) {
|
|
47
|
+
const index = hooks.indexOf(handler);
|
|
48
|
+
if (index > -1) hooks.splice(index, 1);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register an after hook middleware that runs after the operation completes.
|
|
55
|
+
* After hooks cannot halt operations (they run after completion).
|
|
56
|
+
*
|
|
57
|
+
* @param {string} operation - Operation name
|
|
58
|
+
* @param {string} model - Model name
|
|
59
|
+
* @param {Function} handler - Middleware function (context) => void
|
|
60
|
+
* @returns {Function} Unsubscribe function
|
|
61
|
+
*/
|
|
62
|
+
export function afterHook(operation, model, handler) {
|
|
63
|
+
const key = `${operation}:${model}`;
|
|
64
|
+
if (!afterHooks.has(key)) {
|
|
65
|
+
afterHooks.set(key, []);
|
|
66
|
+
}
|
|
67
|
+
afterHooks.get(key).push(handler);
|
|
68
|
+
|
|
69
|
+
// Return unsubscribe function
|
|
70
|
+
return () => {
|
|
71
|
+
const hooks = afterHooks.get(key);
|
|
72
|
+
if (hooks) {
|
|
73
|
+
const index = hooks.indexOf(handler);
|
|
74
|
+
if (index > -1) hooks.splice(index, 1);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get all before hooks for an operation:model combination.
|
|
81
|
+
* @param {string} operation
|
|
82
|
+
* @param {string} model
|
|
83
|
+
* @returns {Function[]}
|
|
84
|
+
*/
|
|
85
|
+
export function getBeforeHooks(operation, model) {
|
|
86
|
+
const key = `${operation}:${model}`;
|
|
87
|
+
return beforeHooks.get(key) || [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get all after hooks for an operation:model combination.
|
|
92
|
+
* @param {string} operation
|
|
93
|
+
* @param {string} model
|
|
94
|
+
* @returns {Function[]}
|
|
95
|
+
*/
|
|
96
|
+
export function getAfterHooks(operation, model) {
|
|
97
|
+
const key = `${operation}:${model}`;
|
|
98
|
+
return afterHooks.get(key) || [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clear registered hooks for a specific operation:model.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} operation - Operation name
|
|
105
|
+
* @param {string} model - Model name
|
|
106
|
+
* @param {string} [type] - 'before' or 'after' (if omitted, clears both)
|
|
107
|
+
*/
|
|
108
|
+
export function clearHook(operation, model, type) {
|
|
109
|
+
const key = `${operation}:${model}`;
|
|
110
|
+
if (!type || type === 'before') {
|
|
111
|
+
beforeHooks.set(key, []);
|
|
112
|
+
}
|
|
113
|
+
if (!type || type === 'after') {
|
|
114
|
+
afterHooks.set(key, []);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear all hooks (useful for testing).
|
|
120
|
+
*/
|
|
121
|
+
export function clearAllHooks() {
|
|
122
|
+
beforeHooks.clear();
|
|
123
|
+
afterHooks.clear();
|
|
124
|
+
}
|
package/src/index.js
CHANGED
|
@@ -15,14 +15,24 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import Model from './model.js';
|
|
18
|
+
import View from './view.js';
|
|
18
19
|
import Serializer from './serializer.js';
|
|
19
20
|
|
|
20
21
|
import attr from './attr.js';
|
|
21
22
|
import belongsTo from './belongs-to.js';
|
|
22
23
|
import hasMany from './has-many.js';
|
|
23
24
|
import { createRecord, updateRecord } from './manage-record.js';
|
|
25
|
+
import { count, avg, sum, min, max } from './aggregates.js';
|
|
24
26
|
|
|
25
27
|
export { default } from './main.js';
|
|
26
28
|
export { store, relationships } from './main.js';
|
|
27
|
-
export { Model, Serializer }; // base classes
|
|
28
|
-
export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
|
|
29
|
+
export { Model, View, Serializer }; // base classes
|
|
30
|
+
export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
|
|
31
|
+
export { count, avg, sum, min, max }; // aggregate helpers
|
|
32
|
+
export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
|
|
33
|
+
|
|
34
|
+
// Store API:
|
|
35
|
+
// store.get(model, id) — sync, memory-only
|
|
36
|
+
// store.find(model, id) — async, MySQL for memory:false models
|
|
37
|
+
// store.findAll(model) — async, all records
|
|
38
|
+
// store.query(model, conditions) — async, always hits MySQL
|
package/src/main.js
CHANGED
|
@@ -19,10 +19,12 @@ import config from 'stonyx/config';
|
|
|
19
19
|
import log from 'stonyx/log';
|
|
20
20
|
import { forEachFileImport } from '@stonyx/utils/file';
|
|
21
21
|
import { kebabCaseToPascalCase, pluralize } from '@stonyx/utils/string';
|
|
22
|
+
import { registerPluralName } from './plural-registry.js';
|
|
22
23
|
import setupRestServer from './setup-rest-server.js';
|
|
23
24
|
import baseTransforms from './transforms.js';
|
|
24
25
|
import Store from './store.js';
|
|
25
26
|
import Serializer from './serializer.js';
|
|
27
|
+
import { setup } from '@stonyx/events';
|
|
26
28
|
|
|
27
29
|
const defaultOptions = {
|
|
28
30
|
dbType: 'json'
|
|
@@ -35,6 +37,7 @@ export default class Orm {
|
|
|
35
37
|
|
|
36
38
|
models = {};
|
|
37
39
|
serializers = {};
|
|
40
|
+
views = {};
|
|
38
41
|
transforms = { ...baseTransforms };
|
|
39
42
|
warnings = new Set();
|
|
40
43
|
|
|
@@ -65,7 +68,10 @@ export default class Orm {
|
|
|
65
68
|
// Transforms keep their original name, everything else gets converted to PascalCase with the type suffix
|
|
66
69
|
const alias = type === 'Transform' ? name : `${kebabCaseToPascalCase(name)}${type}`;
|
|
67
70
|
|
|
68
|
-
if (type === 'Model')
|
|
71
|
+
if (type === 'Model') {
|
|
72
|
+
Orm.store.set(name, new Map());
|
|
73
|
+
registerPluralName(name, exported);
|
|
74
|
+
}
|
|
69
75
|
|
|
70
76
|
return this[pluralize(lowerCaseType)][alias] = exported;
|
|
71
77
|
}, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
|
|
@@ -74,10 +80,44 @@ export default class Orm {
|
|
|
74
80
|
// Wait for imports before db & rest server setup
|
|
75
81
|
await Promise.all(promises);
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
// Discover views from paths.view (separate from model/serializer/transform)
|
|
84
|
+
if (paths.view) {
|
|
85
|
+
await forEachFileImport(paths.view, (exported, { name }) => {
|
|
86
|
+
const alias = `${kebabCaseToPascalCase(name)}View`;
|
|
87
|
+
Orm.store.set(name, new Map());
|
|
88
|
+
registerPluralName(name, exported);
|
|
89
|
+
this.views[alias] = exported;
|
|
90
|
+
}, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Setup event names for hooks after models are loaded
|
|
94
|
+
const eventNames = [];
|
|
95
|
+
const operations = ['list', 'get', 'create', 'update', 'delete'];
|
|
96
|
+
const viewOperations = ['list', 'get'];
|
|
97
|
+
const timings = ['before', 'after'];
|
|
98
|
+
|
|
99
|
+
for (const modelName of Orm.store.data.keys()) {
|
|
100
|
+
const isView = this.isView(modelName);
|
|
101
|
+
const ops = isView ? viewOperations : operations;
|
|
102
|
+
|
|
103
|
+
for (const timing of timings) {
|
|
104
|
+
for (const operation of ops) {
|
|
105
|
+
eventNames.push(`${timing}:${operation}:${modelName}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setup(eventNames);
|
|
111
|
+
|
|
112
|
+
if (config.orm.mysql) {
|
|
113
|
+
const { default: MysqlDB } = await import('./mysql/mysql-db.js');
|
|
114
|
+
this.mysqlDb = new MysqlDB();
|
|
115
|
+
this.db = this.mysqlDb;
|
|
116
|
+
promises.push(this.mysqlDb.init());
|
|
117
|
+
} else if (this.options.dbType !== 'none') {
|
|
78
118
|
const db = new DB();
|
|
79
119
|
this.db = db;
|
|
80
|
-
|
|
120
|
+
|
|
81
121
|
promises.push(db.init());
|
|
82
122
|
}
|
|
83
123
|
|
|
@@ -85,10 +125,29 @@ export default class Orm {
|
|
|
85
125
|
promises.push(setupRestServer(restServer.route, paths.access, restServer.metaRoute));
|
|
86
126
|
}
|
|
87
127
|
|
|
128
|
+
// Wire up memory resolver so store.find() can check model memory flags
|
|
129
|
+
Orm.store._memoryResolver = (modelName) => {
|
|
130
|
+
const { modelClass } = this.getRecordClasses(modelName);
|
|
131
|
+
return modelClass?.memory === true;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Wire up MySQL reference for on-demand queries from store.find()/findAll()
|
|
135
|
+
if (this.mysqlDb) {
|
|
136
|
+
Orm.store._mysqlDb = this.mysqlDb;
|
|
137
|
+
}
|
|
138
|
+
|
|
88
139
|
Orm.ready = await Promise.all(promises);
|
|
89
140
|
Orm.initialized = true;
|
|
90
141
|
}
|
|
91
142
|
|
|
143
|
+
async startup() {
|
|
144
|
+
if (this.mysqlDb) await this.mysqlDb.startup();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async shutdown() {
|
|
148
|
+
if (this.mysqlDb) await this.mysqlDb.shutdown();
|
|
149
|
+
}
|
|
150
|
+
|
|
92
151
|
static get db() {
|
|
93
152
|
if (!Orm.initialized) throw new Error('ORM has not been initialized yet');
|
|
94
153
|
|
|
@@ -97,13 +156,27 @@ export default class Orm {
|
|
|
97
156
|
|
|
98
157
|
getRecordClasses(modelName) {
|
|
99
158
|
const modelClassPrefix = kebabCaseToPascalCase(modelName);
|
|
100
|
-
|
|
159
|
+
|
|
160
|
+
// Check views first, then models
|
|
161
|
+
const viewClass = this.views[`${modelClassPrefix}View`];
|
|
162
|
+
if (viewClass) {
|
|
163
|
+
return {
|
|
164
|
+
modelClass: viewClass,
|
|
165
|
+
serializerClass: this.serializers[`${modelClassPrefix}Serializer`] || Serializer
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
101
169
|
return {
|
|
102
170
|
modelClass: this.models[`${modelClassPrefix}Model`],
|
|
103
171
|
serializerClass: this.serializers[`${modelClassPrefix}Serializer`] || Serializer
|
|
104
172
|
};
|
|
105
173
|
}
|
|
106
174
|
|
|
175
|
+
isView(modelName) {
|
|
176
|
+
const modelClassPrefix = kebabCaseToPascalCase(modelName);
|
|
177
|
+
return !!this.views[`${modelClassPrefix}View`];
|
|
178
|
+
}
|
|
179
|
+
|
|
107
180
|
// Queue warnings to avoid the same error from being logged in the same iteration
|
|
108
181
|
warn(message) {
|
|
109
182
|
this.warnings.add(message);
|
package/src/manage-record.js
CHANGED
|
@@ -14,6 +14,11 @@ export function createRecord(modelName, rawData={}, userOptions={}) {
|
|
|
14
14
|
|
|
15
15
|
if (!initialized && !options.isDbRecord) throw new Error('ORM is not ready');
|
|
16
16
|
|
|
17
|
+
// Guard: read-only views cannot have records created directly
|
|
18
|
+
if (orm?.isView?.(modelName) && !options.isDbRecord) {
|
|
19
|
+
throw new Error(`Cannot create records for read-only view '${modelName}'`);
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
const modelStore = store.get(modelName);
|
|
18
23
|
const globalRelationships = relationships.get('global');
|
|
19
24
|
const pendingRelationships = relationships.get('pending');
|
|
@@ -83,6 +88,12 @@ export function createRecord(modelName, rawData={}, userOptions={}) {
|
|
|
83
88
|
export function updateRecord(record, rawData, userOptions={}) {
|
|
84
89
|
if (!rawData) throw new Error('rawData must be passed in to updateRecord call');
|
|
85
90
|
|
|
91
|
+
// Guard: read-only views cannot be updated
|
|
92
|
+
const modelName = record?.__model?.__name;
|
|
93
|
+
if (modelName && Orm.instance?.isView?.(modelName)) {
|
|
94
|
+
throw new Error(`Cannot update records for read-only view '${modelName}'`);
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
const options = { ...defaultOptions, ...userOptions, update:true };
|
|
87
98
|
|
|
88
99
|
record.serialize(rawData, options);
|
|
@@ -90,14 +101,29 @@ export function updateRecord(record, rawData, userOptions={}) {
|
|
|
90
101
|
|
|
91
102
|
/**
|
|
92
103
|
* gets the next available id based on last record entry.
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* record is created
|
|
104
|
+
*
|
|
105
|
+
* In MySQL mode with numeric IDs, assigns a temporary pending ID.
|
|
106
|
+
* MySQL's AUTO_INCREMENT provides the real ID after INSERT.
|
|
97
107
|
*/
|
|
98
108
|
function assignRecordId(modelName, rawData) {
|
|
99
109
|
if (rawData.id) return;
|
|
100
110
|
|
|
111
|
+
// In MySQL mode with numeric IDs, defer to MySQL auto-increment
|
|
112
|
+
if (Orm.instance?.mysqlDb && !isStringIdModel(modelName)) {
|
|
113
|
+
rawData.id = `__pending_${Date.now()}_${Math.random()}`;
|
|
114
|
+
rawData.__pendingMysqlId = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
const modelStore = Array.from(store.get(modelName).values());
|
|
102
119
|
rawData.id = modelStore.length ? modelStore.at(-1).id + 1 : 1;
|
|
103
120
|
}
|
|
121
|
+
|
|
122
|
+
function isStringIdModel(modelName) {
|
|
123
|
+
const { modelClass } = Orm.instance.getRecordClasses(modelName);
|
|
124
|
+
if (!modelClass) return false;
|
|
125
|
+
|
|
126
|
+
const model = new modelClass(modelName);
|
|
127
|
+
|
|
128
|
+
return model.id?.type === 'string';
|
|
129
|
+
}
|
package/src/migrate.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import config from 'stonyx/config';
|
|
2
|
+
import Orm from '@stonyx/orm';
|
|
3
|
+
import { createFile, createDirectory, readFile, updateFile, deleteDirectory } from '@stonyx/utils/file';
|
|
4
|
+
import { dbKey } from './db.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
function getCollectionKeys() {
|
|
8
|
+
const SchemaClass = Orm.instance.models[`${dbKey}Model`];
|
|
9
|
+
const instance = new SchemaClass();
|
|
10
|
+
const keys = [];
|
|
11
|
+
|
|
12
|
+
for (const key of Object.keys(instance)) {
|
|
13
|
+
if (key === '__name' || key === 'id') continue;
|
|
14
|
+
if (typeof instance[key] === 'function') keys.push(key);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return keys;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getDirPath() {
|
|
21
|
+
const { rootPath } = config;
|
|
22
|
+
const { file, directory } = config.orm.db;
|
|
23
|
+
const dbDir = path.dirname(path.resolve(`${rootPath}/${file}`));
|
|
24
|
+
|
|
25
|
+
return path.join(dbDir, directory);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fileToDirectory() {
|
|
29
|
+
const { rootPath } = config;
|
|
30
|
+
const { file } = config.orm.db;
|
|
31
|
+
const dbFilePath = path.resolve(`${rootPath}/${file}`);
|
|
32
|
+
const collectionKeys = getCollectionKeys();
|
|
33
|
+
const dirPath = getDirPath();
|
|
34
|
+
|
|
35
|
+
// Read full data from db.json
|
|
36
|
+
const data = await readFile(dbFilePath, { json: true });
|
|
37
|
+
|
|
38
|
+
// Create directory and write each collection
|
|
39
|
+
await createDirectory(dirPath);
|
|
40
|
+
|
|
41
|
+
await Promise.all(collectionKeys.map(key =>
|
|
42
|
+
createFile(path.join(dirPath, `${key}.json`), data[key] || [], { json: true })
|
|
43
|
+
));
|
|
44
|
+
|
|
45
|
+
// Overwrite db.json with empty-array skeleton
|
|
46
|
+
const skeleton = {};
|
|
47
|
+
for (const key of collectionKeys) skeleton[key] = [];
|
|
48
|
+
|
|
49
|
+
await updateFile(dbFilePath, skeleton, { json: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function directoryToFile() {
|
|
53
|
+
const { rootPath } = config;
|
|
54
|
+
const { file } = config.orm.db;
|
|
55
|
+
const dbFilePath = path.resolve(`${rootPath}/${file}`);
|
|
56
|
+
const collectionKeys = getCollectionKeys();
|
|
57
|
+
const dirPath = getDirPath();
|
|
58
|
+
|
|
59
|
+
// Read each collection from the directory
|
|
60
|
+
const assembled = {};
|
|
61
|
+
|
|
62
|
+
await Promise.all(collectionKeys.map(async key => {
|
|
63
|
+
const filePath = path.join(dirPath, `${key}.json`);
|
|
64
|
+
assembled[key] = await readFile(filePath, { json: true });
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Overwrite db.json with full assembled data
|
|
68
|
+
await updateFile(dbFilePath, assembled, { json: true });
|
|
69
|
+
|
|
70
|
+
// Remove the directory
|
|
71
|
+
await deleteDirectory(dirPath);
|
|
72
|
+
}
|
package/src/model-property.js
CHANGED
|
@@ -22,8 +22,8 @@ export default class ModelProperty {
|
|
|
22
22
|
return this._value = newValue;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
if (newValue === undefined
|
|
25
|
+
if (newValue === undefined) return;
|
|
26
26
|
|
|
27
|
-
this._value = Orm.instance.transforms[this.type](newValue);
|
|
27
|
+
this._value = newValue === null ? null : Orm.instance.transforms[this.type](newValue);
|
|
28
28
|
}
|
|
29
29
|
}
|
package/src/model.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { attr } from '@stonyx/orm';
|
|
2
2
|
|
|
3
3
|
export default class Model {
|
|
4
|
+
/**
|
|
5
|
+
* Controls whether records of this model are loaded into memory on startup.
|
|
6
|
+
*
|
|
7
|
+
* - true → loaded on boot, kept in store
|
|
8
|
+
* - false → never cached; find() always queries MySQL (default)
|
|
9
|
+
*
|
|
10
|
+
* Override in subclass: static memory = true;
|
|
11
|
+
*/
|
|
12
|
+
static memory = false;
|
|
13
|
+
static pluralName = undefined;
|
|
14
|
+
|
|
4
15
|
id = attr('number');
|
|
5
16
|
|
|
6
17
|
constructor(name) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
let pool = null;
|
|
2
|
+
|
|
3
|
+
export async function getPool(mysqlConfig) {
|
|
4
|
+
if (pool) return pool;
|
|
5
|
+
|
|
6
|
+
const mysql = await import('mysql2/promise');
|
|
7
|
+
|
|
8
|
+
pool = mysql.createPool({
|
|
9
|
+
host: mysqlConfig.host,
|
|
10
|
+
port: mysqlConfig.port,
|
|
11
|
+
user: mysqlConfig.user,
|
|
12
|
+
password: mysqlConfig.password,
|
|
13
|
+
database: mysqlConfig.database,
|
|
14
|
+
connectionLimit: mysqlConfig.connectionLimit,
|
|
15
|
+
waitForConnections: true,
|
|
16
|
+
enableKeepAlive: true,
|
|
17
|
+
keepAliveInitialDelay: 10000,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return pool;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function closePool() {
|
|
24
|
+
if (!pool) return;
|
|
25
|
+
|
|
26
|
+
await pool.end();
|
|
27
|
+
pool = null;
|
|
28
|
+
}
|