sedentary 0.1.0 → 0.1.2
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/dist/cjs/db.js +184 -0
- package/dist/cjs/index.js +599 -0
- package/dist/cjs/package.json +1 -0
- package/dist/es/db.js +177 -0
- package/dist/es/index.js +585 -0
- package/dist/types/db.d.ts +130 -0
- package/dist/types/index.d.ts +165 -0
- package/package.json +4 -18
package/dist/es/index.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { actions, Attribute, base, EntryBase, loaded, size, Table, Transaction, transaction, Type } from "./db";
|
|
2
|
+
export { Attribute, base, DB, deepCopy, deepDiff, EntryBase, loaded, size, Table, Transaction, transaction, Type } from "./db";
|
|
3
|
+
const attributes = Symbol("attributes");
|
|
4
|
+
const methods = Symbol("methods");
|
|
5
|
+
const operators = ["=", ">", "<", ">=", "<=", "<>", "IN", "IS NULL", "LIKE", "NOT"];
|
|
6
|
+
const allowedOption = ["indexes", "int8id", "parent", "primaryKey", "sync", "tableName"];
|
|
7
|
+
const reservedNames = [
|
|
8
|
+
"attr2field",
|
|
9
|
+
"attributeName",
|
|
10
|
+
"cancel",
|
|
11
|
+
"class",
|
|
12
|
+
"construct",
|
|
13
|
+
"constructor",
|
|
14
|
+
"defaultValue",
|
|
15
|
+
"fieldName",
|
|
16
|
+
"foreignKeys",
|
|
17
|
+
"load",
|
|
18
|
+
"modelName",
|
|
19
|
+
"name",
|
|
20
|
+
"postCommit",
|
|
21
|
+
"postLoad",
|
|
22
|
+
"postRemove",
|
|
23
|
+
"postSave",
|
|
24
|
+
"preCommit",
|
|
25
|
+
"preLoad",
|
|
26
|
+
"preRemove",
|
|
27
|
+
"preSave",
|
|
28
|
+
"primaryKey",
|
|
29
|
+
"prototype",
|
|
30
|
+
"remove",
|
|
31
|
+
"save",
|
|
32
|
+
"tableName",
|
|
33
|
+
"type",
|
|
34
|
+
"unique"
|
|
35
|
+
];
|
|
36
|
+
/** Model and attribute names: ASCII letters, digits, underscore; cannot start with digit. */
|
|
37
|
+
const reModelAttributeName = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
38
|
+
/** tableName and fieldName: ASCII lowercase letters, digits, underscore; cannot start with digit. */
|
|
39
|
+
const reSqlName = /^[a-z_][a-z0-9_]*$/;
|
|
40
|
+
/**
|
|
41
|
+
* Converts a JavaScript name to the default SQL name (snake_case).
|
|
42
|
+
* Uppercase letters become lowercase preceded by underscore, except the first character
|
|
43
|
+
* which is only lowercased without underscore.
|
|
44
|
+
*/
|
|
45
|
+
export function toSqlName(name) {
|
|
46
|
+
if (typeof name !== "string" || name.length === 0)
|
|
47
|
+
throw new Error("toSqlName: 'name' must be a non-empty string");
|
|
48
|
+
let result = name[0].toLowerCase();
|
|
49
|
+
for (let i = 1; i < name.length; i++) {
|
|
50
|
+
const c = name[i];
|
|
51
|
+
result += c >= "A" && c <= "Z" ? `_${c.toLowerCase()}` : c;
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export class Sedentary {
|
|
56
|
+
autoSync;
|
|
57
|
+
db;
|
|
58
|
+
doSync = true;
|
|
59
|
+
log;
|
|
60
|
+
models = {};
|
|
61
|
+
constructor(options) {
|
|
62
|
+
if (!options)
|
|
63
|
+
options = {};
|
|
64
|
+
if (!(options instanceof Object))
|
|
65
|
+
throw new Error("new Sedentary: 'options' argument: Wrong type, expected 'Object'");
|
|
66
|
+
for (const k in options)
|
|
67
|
+
if (!["autoSync", "log", "sync"].includes(k))
|
|
68
|
+
throw new Error(`new Sedentary: 'options' argument: Unknown '${k}' option`);
|
|
69
|
+
const { autoSync, log, sync } = { autoSync: true, sync: true, ...options };
|
|
70
|
+
if (typeof autoSync !== "boolean")
|
|
71
|
+
throw new Error("new Sedentary: 'autoSync' option: Wrong type, expected 'boolean'");
|
|
72
|
+
if (log !== null && log !== undefined && !(log instanceof Function))
|
|
73
|
+
throw new Error("new Sedentary: 'log' option: Wrong type, expected 'null' or 'Function'");
|
|
74
|
+
if (typeof sync !== "boolean")
|
|
75
|
+
throw new Error("new Sedentary: 'sync' option: Wrong type, expected 'boolean'");
|
|
76
|
+
this.autoSync = autoSync;
|
|
77
|
+
this.db = null;
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
this.log = log ? log : log === null ? () => { } : console.log;
|
|
80
|
+
this.doSync = sync;
|
|
81
|
+
}
|
|
82
|
+
Boolean(options) {
|
|
83
|
+
return new Type({ ...options, [base]: Boolean, type: "BOOLEAN" });
|
|
84
|
+
}
|
|
85
|
+
DateTime(options) {
|
|
86
|
+
return new Type({ ...options, [base]: Date, type: "DATETIME" });
|
|
87
|
+
}
|
|
88
|
+
FKey(attribute, options) {
|
|
89
|
+
const { attributeName, fieldName, modelName, tableName, type, unique, [base]: _base, [size]: _size } = attribute;
|
|
90
|
+
if (!unique)
|
|
91
|
+
throw new Error(`Sedentary.FKey: '${modelName}' model: '${attributeName}' attribute: is not unique: can't be used as FKey target`);
|
|
92
|
+
return new Type({ [base]: _base, foreignKey: { attributeName, fieldName, options, tableName }, [size]: _size, type });
|
|
93
|
+
}
|
|
94
|
+
Float(options) {
|
|
95
|
+
const sizeFloat = "Sedentary.Float: 'size' argument: Wrong value, expected 4 or 8";
|
|
96
|
+
let storageSize;
|
|
97
|
+
let rest;
|
|
98
|
+
[storageSize, rest] = this.checkSize(sizeFloat, options);
|
|
99
|
+
if (storageSize === undefined)
|
|
100
|
+
storageSize = 8;
|
|
101
|
+
if (storageSize !== 4 && storageSize !== 8)
|
|
102
|
+
throw new Error(sizeFloat);
|
|
103
|
+
return new Type({ ...rest, [base]: Number, [size]: storageSize, type: "FLOAT" });
|
|
104
|
+
}
|
|
105
|
+
Int(options) {
|
|
106
|
+
const sizeInt = "Sedentary.Int: 'size' argument: Wrong value, expected 2 or 4";
|
|
107
|
+
let storageSize;
|
|
108
|
+
let rest;
|
|
109
|
+
[storageSize, rest] = this.checkSize(sizeInt, options);
|
|
110
|
+
if (storageSize === undefined)
|
|
111
|
+
storageSize = 4;
|
|
112
|
+
if (storageSize !== 2 && storageSize !== 4)
|
|
113
|
+
throw new Error(sizeInt);
|
|
114
|
+
return new Type({ ...rest, [base]: Number, [size]: storageSize, type: "INT" });
|
|
115
|
+
}
|
|
116
|
+
Int8(options) {
|
|
117
|
+
return new Type({ ...options, [base]: BigInt, [size]: 8, type: "INT8" });
|
|
118
|
+
}
|
|
119
|
+
JSON(options) {
|
|
120
|
+
return new Type({ ...options, [base]: Object, type: "JSON" });
|
|
121
|
+
}
|
|
122
|
+
Number(options) {
|
|
123
|
+
return new Type({ ...options, [base]: Number, type: "NUMBER" });
|
|
124
|
+
}
|
|
125
|
+
None(options) {
|
|
126
|
+
return new Type({ ...options, [base]: undefined, type: "NONE" });
|
|
127
|
+
}
|
|
128
|
+
VarChar(options) {
|
|
129
|
+
const message = "Sedentary.VarChar: 'size' argument: Wrong value, expected positive integer";
|
|
130
|
+
const [maxSize, rest] = this.checkSize(message, options);
|
|
131
|
+
return new Type({ ...rest, [base]: String, [size]: maxSize, type: "VARCHAR" });
|
|
132
|
+
}
|
|
133
|
+
checkDB() {
|
|
134
|
+
if (!this.db)
|
|
135
|
+
throw new Error("Package sedentary can't be used directly. Please check: https://www.npmjs.com/package/sedentary#disclaimer");
|
|
136
|
+
}
|
|
137
|
+
checkOrderBy(order, attributes, modelName) {
|
|
138
|
+
let array = [];
|
|
139
|
+
if (!order)
|
|
140
|
+
return true;
|
|
141
|
+
if (typeof order === "string")
|
|
142
|
+
array = [order];
|
|
143
|
+
else if (order instanceof Array)
|
|
144
|
+
array = order;
|
|
145
|
+
else
|
|
146
|
+
return false;
|
|
147
|
+
const provided = {};
|
|
148
|
+
for (const attribute of array) {
|
|
149
|
+
if (typeof attribute !== "string")
|
|
150
|
+
return false;
|
|
151
|
+
const attributeName = attribute.startsWith("-") ? attribute.substring(1) : attribute;
|
|
152
|
+
if (!(attributeName in attributes))
|
|
153
|
+
throw new Error(`${modelName}.load: 'order' argument: '${attributeName}' is not an attribute name`);
|
|
154
|
+
if (provided[attributeName])
|
|
155
|
+
throw new Error(`${modelName}.load: 'order' argument: Reused '${attributeName}' attribute`);
|
|
156
|
+
provided[attributeName] = true;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
checkSize(message, options) {
|
|
161
|
+
if (!options)
|
|
162
|
+
return [undefined, {}];
|
|
163
|
+
const { size, ...rest } = options;
|
|
164
|
+
if (size === undefined)
|
|
165
|
+
return [undefined, rest];
|
|
166
|
+
if (typeof size !== "number")
|
|
167
|
+
throw new Error(message);
|
|
168
|
+
if (size !== parseInt(size.toString(), 10))
|
|
169
|
+
throw new Error(message);
|
|
170
|
+
if (size <= 0)
|
|
171
|
+
throw new Error(message);
|
|
172
|
+
return [size, rest];
|
|
173
|
+
}
|
|
174
|
+
createWhere(modelName, attributes, where) {
|
|
175
|
+
if (typeof where === "string")
|
|
176
|
+
return [where, true, true];
|
|
177
|
+
if (typeof where !== "object")
|
|
178
|
+
throw new Error(`${modelName}.load: 'where' argument: Wrong type, expected 'Array', 'Object' or 'string'`);
|
|
179
|
+
if (!where)
|
|
180
|
+
return ["", false, false];
|
|
181
|
+
if (where instanceof Array) {
|
|
182
|
+
const length = where.length;
|
|
183
|
+
if (!length)
|
|
184
|
+
throw new Error(`${modelName}.load: 'where' argument: Empty Array`);
|
|
185
|
+
if (!["AND", "NOT", "OR"].includes(where[0]))
|
|
186
|
+
throw new Error(`${modelName}.load: 'where' argument: Wrong logical operator, expected 'AND', 'OR' or 'NOT'`);
|
|
187
|
+
if (length === 1)
|
|
188
|
+
return ["", false, false];
|
|
189
|
+
if (where[0] === "NOT") {
|
|
190
|
+
if (length > 2)
|
|
191
|
+
throw new Error(`${modelName}.load: 'where' argument: 'NOT' operator is unary`);
|
|
192
|
+
const [res] = this.createWhere(modelName, attributes, where[1]);
|
|
193
|
+
return [res === "" ? "" : `NOT (${res})`, false, false];
|
|
194
|
+
}
|
|
195
|
+
const conditions = where
|
|
196
|
+
.filter((_, i) => i)
|
|
197
|
+
.map(_ => this.createWhere(modelName, attributes, _))
|
|
198
|
+
.filter(([_]) => _);
|
|
199
|
+
if (conditions.length === 1)
|
|
200
|
+
return conditions[0];
|
|
201
|
+
const isOr = where[0] === "OR";
|
|
202
|
+
return [isOr ? conditions.map(([_, , a]) => (a ? `(${_})` : _)).join(" OR ") : conditions.map(([_, o]) => (o ? `(${_})` : _)).join(" AND "), isOr, false];
|
|
203
|
+
}
|
|
204
|
+
const conditions = [];
|
|
205
|
+
for (const key in where) {
|
|
206
|
+
const field = attributes[key];
|
|
207
|
+
if (!field)
|
|
208
|
+
throw new Error(`${modelName}.load: 'where' argument: Unknown '${key}' attribute`);
|
|
209
|
+
const value = where[key];
|
|
210
|
+
if (value instanceof Array) {
|
|
211
|
+
const operator = value[0];
|
|
212
|
+
const length = value.length;
|
|
213
|
+
if (!length)
|
|
214
|
+
throw new Error(`${modelName}.load: 'where' argument: Missing arithmetic operator, expected one of: ${operators.map(_ => `'${_}'`).join(", ")}`);
|
|
215
|
+
if (!operators.includes(operator))
|
|
216
|
+
throw new Error(`${modelName}.load: 'where' argument: Wrong arithmetic operator, expected one of: ${operators.map(_ => `'${_}'`).join(", ")}`);
|
|
217
|
+
if (operator === "IS NULL") {
|
|
218
|
+
if (length !== 1)
|
|
219
|
+
throw new Error(`${modelName}.load: 'where' argument: 'IS NULL' operator is unary`);
|
|
220
|
+
conditions.push(`${field} IS NULL`);
|
|
221
|
+
}
|
|
222
|
+
else if (operator === "NOT") {
|
|
223
|
+
if (length !== 1)
|
|
224
|
+
throw new Error(`${modelName}.load: 'where' argument: 'NOT' operator is unary`);
|
|
225
|
+
conditions.push(`NOT ${field}`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
if (length !== 2)
|
|
229
|
+
throw new Error(`${modelName}.load: 'where' argument: '${operator}' operator is binary`);
|
|
230
|
+
if (operator === "IN") {
|
|
231
|
+
if (!(value[1] instanceof Array))
|
|
232
|
+
throw new Error(`${modelName}.load: 'where' argument: 'IN' right operand: Wrong type, expected Array`);
|
|
233
|
+
conditions.push(`${field} IN (${value[1].map(_ => this.escape(_)).join(", ")})`);
|
|
234
|
+
}
|
|
235
|
+
else
|
|
236
|
+
conditions.push(`${field} ${operator} ${this.escape(value[1])}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
conditions.push(`${field} = ${this.escape(value)}`);
|
|
241
|
+
}
|
|
242
|
+
return [conditions.length ? conditions.join(" AND ") : "", false, false];
|
|
243
|
+
}
|
|
244
|
+
async connect(sync) {
|
|
245
|
+
try {
|
|
246
|
+
this.checkDB();
|
|
247
|
+
this.log("Connecting...");
|
|
248
|
+
await this.db.connect();
|
|
249
|
+
this.log("Connected");
|
|
250
|
+
if (this.autoSync || sync)
|
|
251
|
+
await this.sync();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
this.log(`Connecting: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async sync() {
|
|
259
|
+
this.log("Syncing...");
|
|
260
|
+
await this.db.syncDataBase();
|
|
261
|
+
this.log("Synced");
|
|
262
|
+
}
|
|
263
|
+
async end() {
|
|
264
|
+
this.log("Closing connection...");
|
|
265
|
+
await this.db.end();
|
|
266
|
+
this.log("Connection closed");
|
|
267
|
+
}
|
|
268
|
+
begin() {
|
|
269
|
+
return this.db.begin();
|
|
270
|
+
}
|
|
271
|
+
escape(value) {
|
|
272
|
+
return this.db.escape(value);
|
|
273
|
+
}
|
|
274
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
275
|
+
model(modelName, _attributes, options, _methods) {
|
|
276
|
+
this.checkDB();
|
|
277
|
+
if (typeof modelName !== "string")
|
|
278
|
+
throw new Error("Sedentary.model: 'name' argument: Wrong type, expected 'string'");
|
|
279
|
+
if (!reModelAttributeName.test(modelName))
|
|
280
|
+
throw new Error(`Sedentary.model: '${modelName}' model: Invalid model name, expected ASCII letters, digits or underscore, cannot start with digit`);
|
|
281
|
+
if (this.models[modelName])
|
|
282
|
+
throw new Error(`Sedentary.model: '${modelName}' model: Model already defined`);
|
|
283
|
+
if (!_attributes)
|
|
284
|
+
_attributes = {};
|
|
285
|
+
if (!(_attributes instanceof Object))
|
|
286
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'attributes' argument: Wrong type, expected 'Object'`);
|
|
287
|
+
if (!options)
|
|
288
|
+
options = {};
|
|
289
|
+
if (!(options instanceof Object))
|
|
290
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'options' argument: Wrong type, expected 'Object'`);
|
|
291
|
+
for (const k in options)
|
|
292
|
+
if (!allowedOption.includes(k))
|
|
293
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'options' argument: Unknown '${k}' option`);
|
|
294
|
+
if (options.int8id && options.parent)
|
|
295
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'int8id' and 'parent' options conflict each other`);
|
|
296
|
+
if (options.int8id && options.primaryKey)
|
|
297
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'int8id' and 'primaryKey' options conflict each other`);
|
|
298
|
+
if (options.parent && options.primaryKey)
|
|
299
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'parent' and 'primaryKey' options conflict each other`);
|
|
300
|
+
let autoIncrement = true;
|
|
301
|
+
const { indexes, int8id, parent, primaryKey, sync, tableName } = { sync: this.doSync, tableName: toSqlName(modelName), ...options };
|
|
302
|
+
if (!reSqlName.test(tableName))
|
|
303
|
+
throw new Error(`Sedentary.model: '${modelName}' model: Invalid tableName '${tableName}', expected lowercase ASCII letters, digits or underscore, cannot start with digit`);
|
|
304
|
+
let aArray = int8id
|
|
305
|
+
? [new Attribute({ ...this.Int8(), attributeName: "id", fieldName: toSqlName("id"), modelName, notNull: true, tableName, unique: true })]
|
|
306
|
+
: [new Attribute({ ...this.Int({ size: 4 }), attributeName: "id", fieldName: toSqlName("id"), modelName, notNull: true, tableName, unique: true })];
|
|
307
|
+
let constraints = [{ attribute: aArray[0], constraintName: `${tableName}_id_unique`, type: "u" }];
|
|
308
|
+
const iArray = [];
|
|
309
|
+
let pk = aArray[0];
|
|
310
|
+
let attr2field = { id: "id" };
|
|
311
|
+
if (!_methods)
|
|
312
|
+
_methods = {};
|
|
313
|
+
if (!(_methods instanceof Object))
|
|
314
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'methods' option: Wrong type, expected 'Object'`);
|
|
315
|
+
if (parent)
|
|
316
|
+
if (!parent[attributes])
|
|
317
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'parent' option: Wrong type, expected 'Model'`);
|
|
318
|
+
if (primaryKey && typeof primaryKey !== "string")
|
|
319
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'primaryKey' option: Wrong type, expected 'string'`);
|
|
320
|
+
if (primaryKey && !Object.keys(_attributes).includes(primaryKey))
|
|
321
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'primaryKey' option: Attribute '${primaryKey}' does not exists`);
|
|
322
|
+
if (parent || primaryKey) {
|
|
323
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
324
|
+
attr2field = parent ? { ...parent.attr2field } : {};
|
|
325
|
+
autoIncrement = false;
|
|
326
|
+
aArray = [];
|
|
327
|
+
constraints = [];
|
|
328
|
+
}
|
|
329
|
+
for (const attributeName of Object.keys(_attributes).sort()) {
|
|
330
|
+
if (!reModelAttributeName.test(attributeName))
|
|
331
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: Invalid attribute name, expected ASCII letters, digits or underscore, cannot start with digit`);
|
|
332
|
+
if (reservedNames.includes(attributeName))
|
|
333
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: Reserved name`);
|
|
334
|
+
const attributeDefinition = _attributes[attributeName];
|
|
335
|
+
let { [base]: _base, defaultValue, fieldName, foreignKey, notNull, [size]: _size, type, unique } = (() => {
|
|
336
|
+
if (!(attributeDefinition instanceof Type))
|
|
337
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: '${attributeName}': Wrong type, expected 'Type'`);
|
|
338
|
+
const ret = new Attribute({ attributeName, fieldName: toSqlName(attributeName), modelName, notNull: false, tableName, unique: false, ...attributeDefinition });
|
|
339
|
+
const { defaultValue, fieldName, notNull, unique } = ret;
|
|
340
|
+
if (defaultValue === null)
|
|
341
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'defaultValue' option: Does 'null' default value really make sense?`);
|
|
342
|
+
if (typeof fieldName !== "string")
|
|
343
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'fieldName' option: Wrong type, expected 'string'`);
|
|
344
|
+
if (!reSqlName.test(fieldName)) {
|
|
345
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: Invalid fieldName '${fieldName}', ` +
|
|
346
|
+
"expected lowercase ASCII letters, digits or underscore, cannot start with digit");
|
|
347
|
+
}
|
|
348
|
+
if (typeof notNull !== "boolean")
|
|
349
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'notNull' option: Wrong type, expected 'boolean'`);
|
|
350
|
+
if (typeof unique !== "boolean")
|
|
351
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'unique' option: Wrong type, expected 'boolean'`);
|
|
352
|
+
const { [base]: _base, defaultValue: _defaultValue } = ret;
|
|
353
|
+
if (_defaultValue !== undefined) {
|
|
354
|
+
if (_base === BigInt && typeof _defaultValue !== "bigint")
|
|
355
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'defaultValue' option: Wrong type, expected 'BigInt'`);
|
|
356
|
+
if (_base === Date && !(_defaultValue instanceof Date))
|
|
357
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'defaultValue' option: Wrong type, expected 'Date'`);
|
|
358
|
+
if (_base === Number && typeof _defaultValue !== "number")
|
|
359
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'defaultValue' option: Wrong type, expected 'number'`);
|
|
360
|
+
if (_base === String && typeof _defaultValue !== "string")
|
|
361
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attributeName}' attribute: 'defaultValue' option: Wrong type, expected 'string'`);
|
|
362
|
+
}
|
|
363
|
+
return ret;
|
|
364
|
+
})();
|
|
365
|
+
if (foreignKey) {
|
|
366
|
+
const options = foreignKey.options || {};
|
|
367
|
+
if (foreignKey.options !== undefined && !(foreignKey.options instanceof Object))
|
|
368
|
+
throw new Error(`Sedentary.FKey: '${modelName}' model: '${attributeName}' attribute: Wrong options type, expected 'Object'`);
|
|
369
|
+
for (const k in options)
|
|
370
|
+
if (!["onDelete", "onUpdate"].includes(k))
|
|
371
|
+
throw new Error(`Sedentary.FKey: '${modelName}' model: '${attributeName}' attribute: Unknown option '${k}'`);
|
|
372
|
+
for (const onChange of ["onDelete", "onUpdate"]) {
|
|
373
|
+
const actions = ["cascade", "no action", "restrict", "set default", "set null"];
|
|
374
|
+
let action = options[onChange];
|
|
375
|
+
if (!action)
|
|
376
|
+
action = options[onChange] = "no action";
|
|
377
|
+
if (action && !actions.includes(action))
|
|
378
|
+
throw new Error(`Sedentary.FKey: '${modelName}' model: '${attributeName}' attribute: '${onChange}' option: Wrong value, expected ${actions.map(_ => `'${_}'`).join(" | ")}`);
|
|
379
|
+
}
|
|
380
|
+
foreignKey.options = options;
|
|
381
|
+
}
|
|
382
|
+
if (primaryKey === attributeName) {
|
|
383
|
+
notNull = true;
|
|
384
|
+
unique = true;
|
|
385
|
+
}
|
|
386
|
+
if (defaultValue)
|
|
387
|
+
notNull = true;
|
|
388
|
+
const attribute = new Attribute({ attributeName, [base]: _base, defaultValue, fieldName, foreignKey, modelName, notNull, [size]: _size, tableName, type, unique });
|
|
389
|
+
if (primaryKey === attributeName)
|
|
390
|
+
pk = attribute;
|
|
391
|
+
aArray.push(attribute);
|
|
392
|
+
if (type !== "NONE")
|
|
393
|
+
attr2field[attributeName] = fieldName;
|
|
394
|
+
if (foreignKey)
|
|
395
|
+
constraints.push({ attribute, constraintName: `fkey_${fieldName}_${foreignKey.tableName}_${foreignKey.fieldName}`, type: "f" });
|
|
396
|
+
if (unique)
|
|
397
|
+
constraints.push({ attribute, constraintName: `${tableName}_${fieldName}_unique`, type: "u" });
|
|
398
|
+
}
|
|
399
|
+
if (indexes) {
|
|
400
|
+
const originalAttributes = _attributes;
|
|
401
|
+
if (!(indexes instanceof Object))
|
|
402
|
+
throw new Error(`Sedentary.model: '${modelName}' model: 'indexes' option: Wrong type, expected 'Object'`);
|
|
403
|
+
for (const indexName in indexes) {
|
|
404
|
+
if (aArray.some(({ fieldName, unique }) => unique && `${tableName}_${fieldName}_unique` === indexName))
|
|
405
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: index name already inferred by the unique constraint on an attribute`);
|
|
406
|
+
const idx = indexes[indexName];
|
|
407
|
+
const checkAttribute = (attribute, l) => {
|
|
408
|
+
if (typeof attribute !== "string")
|
|
409
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: #${l + 1} attribute: Wrong type, expected 'string'`);
|
|
410
|
+
if (!(attribute in originalAttributes))
|
|
411
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: #${l + 1} attribute: Unknown attribute '${attribute}'`);
|
|
412
|
+
};
|
|
413
|
+
let attributes;
|
|
414
|
+
let type = "btree";
|
|
415
|
+
let unique = false;
|
|
416
|
+
if (idx instanceof Array) {
|
|
417
|
+
idx.forEach(checkAttribute);
|
|
418
|
+
attributes = idx;
|
|
419
|
+
}
|
|
420
|
+
else if (typeof idx === "string") {
|
|
421
|
+
checkAttribute(idx, 0);
|
|
422
|
+
attributes = [idx];
|
|
423
|
+
}
|
|
424
|
+
else if (idx instanceof Object) {
|
|
425
|
+
for (const k in idx)
|
|
426
|
+
if (!["attributes", "type", "unique"].includes(k))
|
|
427
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: Unknown index option '${k}'`);
|
|
428
|
+
({ attributes, type, unique } = { type: "btree", unique: false, ...idx });
|
|
429
|
+
if (!attributes)
|
|
430
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: Missing 'attributes' option`);
|
|
431
|
+
if (attributes instanceof Array)
|
|
432
|
+
attributes.forEach(checkAttribute);
|
|
433
|
+
else if (typeof attributes === "string") {
|
|
434
|
+
checkAttribute(attributes, 0);
|
|
435
|
+
attributes = [attributes];
|
|
436
|
+
}
|
|
437
|
+
else
|
|
438
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: 'attributes' option: Wrong type, expected 'FieldNames'`);
|
|
439
|
+
if (typeof type !== "string")
|
|
440
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: 'type' option: Wrong type, expected 'string'`);
|
|
441
|
+
if (!["btree", "hash"].includes(type))
|
|
442
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: 'type' option: Wrong value, expected 'btree' or 'hash'`);
|
|
443
|
+
if (typeof unique !== "boolean")
|
|
444
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: 'unique' option: Wrong type, expected 'boolean'`);
|
|
445
|
+
}
|
|
446
|
+
else
|
|
447
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${indexName}' index: Wrong type, expected 'Object'`);
|
|
448
|
+
iArray.push({ fields: attributes, indexName, type, unique });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
this.models[modelName] = true;
|
|
452
|
+
const foreignKeys = aArray
|
|
453
|
+
.filter(_ => _.foreignKey)
|
|
454
|
+
.reduce((ret, curr) => {
|
|
455
|
+
ret[curr.attributeName] = true;
|
|
456
|
+
return ret;
|
|
457
|
+
}, {});
|
|
458
|
+
for (const foreignKey in foreignKeys) {
|
|
459
|
+
if (`${foreignKey}Load` in _attributes)
|
|
460
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${foreignKey}' attribute: '${foreignKey}Load' inferred methods conflicts with an attribute`);
|
|
461
|
+
if (`${foreignKey}Load` in _methods)
|
|
462
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${foreignKey}' attribute: '${foreignKey}Load' inferred methods conflicts with a method`);
|
|
463
|
+
}
|
|
464
|
+
for (const method in _methods)
|
|
465
|
+
if (method in _attributes)
|
|
466
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${method}' method: conflicts with an attribute`);
|
|
467
|
+
const checkParent = (parent) => {
|
|
468
|
+
if (!parent)
|
|
469
|
+
return;
|
|
470
|
+
for (const attribute in _attributes) {
|
|
471
|
+
if (attribute in parent[attributes])
|
|
472
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attribute}' attribute: conflicts with an attribute of '${parent.modelName}' model`);
|
|
473
|
+
if (attribute in parent[methods])
|
|
474
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attribute}' attribute: conflicts with a method of '${parent.modelName}' model`);
|
|
475
|
+
for (const foreignKey in parent.foreignKeys)
|
|
476
|
+
if (attribute === `${foreignKey}Load`)
|
|
477
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${attribute}' attribute: conflicts with an inferred methods of '${parent.modelName}' model`);
|
|
478
|
+
}
|
|
479
|
+
for (const foreignKey in foreignKeys) {
|
|
480
|
+
if (`${foreignKey}Load` in parent[attributes])
|
|
481
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${foreignKey}' attribute: '${foreignKey}Load' inferred methods conflicts with an attribute of '${parent.modelName}' model`);
|
|
482
|
+
if (`${foreignKey}Load` in parent[methods])
|
|
483
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${foreignKey}' attribute: '${foreignKey}Load' inferred methods conflicts with a method of '${parent.modelName}' model`);
|
|
484
|
+
}
|
|
485
|
+
for (const method in _methods) {
|
|
486
|
+
if (method in parent[attributes])
|
|
487
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${method}' method: conflicts with an attribute of '${parent.modelName}' model`);
|
|
488
|
+
for (const foreignKey in parent.foreignKeys)
|
|
489
|
+
if (`${foreignKey}Load` === method)
|
|
490
|
+
throw new Error(`Sedentary.model: '${modelName}' model: '${method}' method: conflicts with an inferred methods of '${parent.modelName}' model`);
|
|
491
|
+
}
|
|
492
|
+
checkParent(parent.parent);
|
|
493
|
+
};
|
|
494
|
+
checkParent(parent);
|
|
495
|
+
const ret = class extends (parent || EntryBase) {
|
|
496
|
+
constructor(from, tx) {
|
|
497
|
+
super(from);
|
|
498
|
+
if (tx)
|
|
499
|
+
tx.addEntry(this);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const table = new Table({ attributes: aArray, autoIncrement, constraints, indexes: iArray, model: ret, parent, pk, sync, tableName });
|
|
503
|
+
this.db.tables.push(table);
|
|
504
|
+
const cancel_ = this.db.cancel(tableName);
|
|
505
|
+
const cancel = (where, tx) => cancel_(this.createWhere(modelName, attr2field, where)[0], tx);
|
|
506
|
+
Object.defineProperty(cancel, "name", { value: `${modelName}.cancel` });
|
|
507
|
+
const load_ = this.db.load(tableName, attr2field, pk, ret, table);
|
|
508
|
+
const load = (where, ...args) => {
|
|
509
|
+
let order = undefined;
|
|
510
|
+
let limit = undefined;
|
|
511
|
+
let tx = undefined;
|
|
512
|
+
let lock = undefined;
|
|
513
|
+
const checkArgs = (first) => {
|
|
514
|
+
if (!args.length)
|
|
515
|
+
return;
|
|
516
|
+
if (args[0] instanceof Transaction) {
|
|
517
|
+
if (first)
|
|
518
|
+
order = undefined;
|
|
519
|
+
limit = undefined;
|
|
520
|
+
[tx, lock] = args;
|
|
521
|
+
}
|
|
522
|
+
else if (typeof args[0] === "number") {
|
|
523
|
+
if (first)
|
|
524
|
+
order = undefined;
|
|
525
|
+
[limit, tx, lock] = args;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
if (first) {
|
|
529
|
+
order = args.shift();
|
|
530
|
+
checkArgs(false);
|
|
531
|
+
}
|
|
532
|
+
else
|
|
533
|
+
throw new Error(`${modelName}.load: 'limit' argument: Wrong type, expected 'number'`);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
checkArgs(true);
|
|
537
|
+
if (!this.checkOrderBy(order, attr2field, modelName))
|
|
538
|
+
throw new Error(`${modelName}.load: 'order' argument: Wrong type, expected 'string | string[]'`);
|
|
539
|
+
if (tx && !(tx instanceof Transaction))
|
|
540
|
+
throw new Error(`${modelName}.load: 'tx' argument: Wrong type, expected 'Transaction'`);
|
|
541
|
+
return load_(this.createWhere(modelName, attr2field, where)[0], order, limit, tx, lock);
|
|
542
|
+
};
|
|
543
|
+
Object.defineProperty(load, "name", { value: `${modelName}.load` });
|
|
544
|
+
Object.defineProperty(ret, "cancel", { value: cancel });
|
|
545
|
+
Object.defineProperty(ret, "name", { value: modelName });
|
|
546
|
+
Object.defineProperty(ret, "load", { value: load });
|
|
547
|
+
Object.defineProperty(ret, "attr2field", { value: attr2field });
|
|
548
|
+
Object.defineProperty(ret, attributes, { value: _attributes });
|
|
549
|
+
Object.defineProperty(ret, "foreignKeys", { value: foreignKeys });
|
|
550
|
+
Object.defineProperty(ret, methods, { value: _methods });
|
|
551
|
+
Object.assign(ret.prototype, _methods);
|
|
552
|
+
const ensureActions = (entry) => {
|
|
553
|
+
if (!entry[actions])
|
|
554
|
+
Object.defineProperty(entry, actions, { configurable: true, value: [] });
|
|
555
|
+
return entry[actions];
|
|
556
|
+
};
|
|
557
|
+
const remove = this.db.remove(tableName, pk);
|
|
558
|
+
ret.prototype.remove = async function () {
|
|
559
|
+
if (!this[loaded])
|
|
560
|
+
throw new Error(`${modelName}.remove: Can't remove a never saved Entry`);
|
|
561
|
+
this.preRemove();
|
|
562
|
+
const records = await remove.call(this);
|
|
563
|
+
this.postRemove(records);
|
|
564
|
+
if (this[transaction])
|
|
565
|
+
ensureActions(this).push({ action: "remove", records });
|
|
566
|
+
return records;
|
|
567
|
+
};
|
|
568
|
+
Object.defineProperty(ret.prototype.remove, "name", { value: `${modelName}.remove` });
|
|
569
|
+
const save = this.db.save(tableName, attr2field, pk);
|
|
570
|
+
ret.prototype.save = async function () {
|
|
571
|
+
this.preSave();
|
|
572
|
+
const records = await save.call(this);
|
|
573
|
+
this.postSave(records);
|
|
574
|
+
if (this[transaction])
|
|
575
|
+
ensureActions(this).push({ action: "save", records });
|
|
576
|
+
return records;
|
|
577
|
+
};
|
|
578
|
+
Object.defineProperty(ret.prototype.save, "name", { value: `${modelName}.save` });
|
|
579
|
+
for (const attribute of aArray)
|
|
580
|
+
Object.defineProperty(ret, attribute.attributeName, { value: attribute });
|
|
581
|
+
for (const key of ["attributeName", base, "fieldName", "modelName", size, "tableName", "type", "unique"])
|
|
582
|
+
Object.defineProperty(ret, key, { value: pk[key] });
|
|
583
|
+
return ret;
|
|
584
|
+
}
|
|
585
|
+
}
|