db-model-router 1.0.0
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/.env +7 -0
- package/LICENSE +201 -0
- package/README.md +505 -0
- package/docker-compose.yml +141 -0
- package/docs/README.md +208 -0
- package/docs/SKILL.md +202 -0
- package/docs/adapters/cockroachdb.md +49 -0
- package/docs/adapters/dynamodb.md +53 -0
- package/docs/adapters/mongodb.md +56 -0
- package/docs/adapters/mssql.md +55 -0
- package/docs/adapters/oracle.md +52 -0
- package/docs/adapters/postgres.md +50 -0
- package/docs/adapters/redis.md +53 -0
- package/docs/adapters/sqlite3.md +43 -0
- package/package.json +109 -0
- package/src/cli/generate-app.js +359 -0
- package/src/cli/generate-model.js +760 -0
- package/src/cli/generate-openapi.js +237 -0
- package/src/cli/generate-route.js +346 -0
- package/src/cockroachdb/db.js +563 -0
- package/src/commons/function.js +165 -0
- package/src/commons/model.js +444 -0
- package/src/commons/route.js +214 -0
- package/src/commons/validator.js +172 -0
- package/src/dynamodb/db.js +552 -0
- package/src/index.js +57 -0
- package/src/mongodb/db.js +381 -0
- package/src/mssql/db.js +461 -0
- package/src/mysql/db.js +527 -0
- package/src/oracle/db.js +855 -0
- package/src/oracle/sql_translator.js +406 -0
- package/src/postgres/db.js +666 -0
- package/src/postgres/ddl_translator.js +69 -0
- package/src/postgres/sql_translator.js +396 -0
- package/src/redis/db.js +448 -0
- package/src/serve.js +90 -0
- package/src/sqlite3/db.js +346 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
function jsonSafeParse(obj) {
|
|
2
|
+
if (typeof obj === "string") {
|
|
3
|
+
try {
|
|
4
|
+
// If the string is a bare numeric literal that would lose precision,
|
|
5
|
+
// return it as-is (string) to preserve the original value.
|
|
6
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(obj.trim())) {
|
|
7
|
+
if (wouldLosePrecision(obj.trim())) {
|
|
8
|
+
return obj;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// Pre-process: find numeric literals in the JSON that would lose precision.
|
|
12
|
+
// A number loses precision if:
|
|
13
|
+
// - It's an integer with more than 15 significant digits
|
|
14
|
+
// - It's a float with more than 15 significant digits total
|
|
15
|
+
// - It exceeds Number.MAX_SAFE_INTEGER (9007199254740991)
|
|
16
|
+
//
|
|
17
|
+
// We wrap those in quotes before JSON.parse sees them.
|
|
18
|
+
const safed = obj.replace(
|
|
19
|
+
/(?<=[:,\[]\s*)-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?(?=\s*[,\}\]])/g,
|
|
20
|
+
(match) => {
|
|
21
|
+
// Check if this number would lose precision
|
|
22
|
+
if (wouldLosePrecision(match)) {
|
|
23
|
+
return '"' + match + '"';
|
|
24
|
+
}
|
|
25
|
+
return match;
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
return JSON.parse(safed);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// If the regex approach fails, fall back to standard reviver
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(obj, (key, value) => {
|
|
33
|
+
if (
|
|
34
|
+
typeof value === "number" &&
|
|
35
|
+
!Number.isSafeInteger(value) &&
|
|
36
|
+
!isSmallFloat(value)
|
|
37
|
+
) {
|
|
38
|
+
return value.toString();
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
});
|
|
42
|
+
} catch (err2) {
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
47
|
+
for (const i in obj) {
|
|
48
|
+
obj[i] = jsonSafeParse(obj[i]);
|
|
49
|
+
}
|
|
50
|
+
return obj;
|
|
51
|
+
} else {
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a numeric string would lose precision when parsed as a JS number.
|
|
58
|
+
*/
|
|
59
|
+
function wouldLosePrecision(numStr) {
|
|
60
|
+
// Remove sign
|
|
61
|
+
const abs = numStr.replace(/^-/, "");
|
|
62
|
+
|
|
63
|
+
// Handle scientific notation
|
|
64
|
+
if (/[eE]/.test(abs)) {
|
|
65
|
+
const num = Number(numStr);
|
|
66
|
+
// If re-stringifying doesn't match, precision was lost
|
|
67
|
+
return num.toString() !== numStr && String(+numStr) !== numStr;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Split integer and decimal parts
|
|
71
|
+
const parts = abs.split(".");
|
|
72
|
+
const intPart = parts[0] || "0";
|
|
73
|
+
const decPart = parts[1] || "";
|
|
74
|
+
|
|
75
|
+
// Count significant digits (strip leading zeros from integer, trailing zeros from decimal)
|
|
76
|
+
const sigDigits =
|
|
77
|
+
(intPart === "0" ? "" : intPart).replace(/^0+/, "") + decPart;
|
|
78
|
+
const significantCount = sigDigits.replace(/^0+/, "").length;
|
|
79
|
+
|
|
80
|
+
// JS can represent ~15-17 significant digits accurately for most values.
|
|
81
|
+
// For integers specifically, anything <= MAX_SAFE_INTEGER is fine regardless of digit count.
|
|
82
|
+
if (significantCount > 16) return true;
|
|
83
|
+
|
|
84
|
+
// For integers, also check against MAX_SAFE_INTEGER
|
|
85
|
+
if (!decPart) {
|
|
86
|
+
const n = Number(numStr);
|
|
87
|
+
if (!Number.isSafeInteger(n)) return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Round-trip check: parse and re-stringify
|
|
91
|
+
const n = Number(numStr);
|
|
92
|
+
if (String(n) !== numStr && n.toString() !== numStr) {
|
|
93
|
+
// Could be trailing zeros in decimal (e.g., "1.50") — that's not precision loss
|
|
94
|
+
if (decPart && Number(numStr) === parseFloat(numStr)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a float is small enough that toString() preserves it.
|
|
105
|
+
* Floats within normal range with <= 15 significant digits are fine.
|
|
106
|
+
*/
|
|
107
|
+
function isSmallFloat(value) {
|
|
108
|
+
if (!Number.isFinite(value)) return false;
|
|
109
|
+
if (Number.isInteger(value)) return Number.isSafeInteger(value);
|
|
110
|
+
// For floats, check if round-trip preserves the value
|
|
111
|
+
return value === parseFloat(value.toString());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function jsonStringify(obj) {
|
|
115
|
+
if (typeof obj === "object") {
|
|
116
|
+
if (!Array.isArray(obj)) {
|
|
117
|
+
for (const i in obj) {
|
|
118
|
+
if (typeof obj[i] === "object") {
|
|
119
|
+
if (obj[i] != null) obj[i] = JSON.stringify(obj[i]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return obj;
|
|
123
|
+
} else {
|
|
124
|
+
for (const i in obj) {
|
|
125
|
+
if (obj[i] != null) obj[i] = jsonStringify(obj[i]);
|
|
126
|
+
}
|
|
127
|
+
return obj;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
return obj;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getType(obj) {
|
|
135
|
+
if (Array.isArray(obj)) {
|
|
136
|
+
return "array";
|
|
137
|
+
} else if (obj === null || obj === undefined) {
|
|
138
|
+
return "null";
|
|
139
|
+
} else {
|
|
140
|
+
return typeof obj;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function empty(obj) {
|
|
145
|
+
return obj === null || obj === undefined || obj === "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function objectSelecter(obj, picker) {
|
|
149
|
+
for (let i of picker) {
|
|
150
|
+
if (obj.hasOwnProperty(i)) {
|
|
151
|
+
obj = obj[i];
|
|
152
|
+
} else {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return obj;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
jsonSafeParse,
|
|
161
|
+
jsonStringify,
|
|
162
|
+
getType,
|
|
163
|
+
empty,
|
|
164
|
+
objectSelecter,
|
|
165
|
+
};
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
const {
|
|
2
|
+
RemovePK,
|
|
3
|
+
getPayloadValidator,
|
|
4
|
+
validateInput,
|
|
5
|
+
dataToFilter,
|
|
6
|
+
RemoveUnknownData,
|
|
7
|
+
} = require("./validator");
|
|
8
|
+
const { getType, jsonStringify, jsonSafeParse } = require("./function");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract and remove reserved params from a data/payload object.
|
|
12
|
+
* Returns { select_columns: string[]|null, output_content_type: string|null, cleaned: data }
|
|
13
|
+
*/
|
|
14
|
+
function extractReservedParams(data) {
|
|
15
|
+
let select_columns = null;
|
|
16
|
+
let output_content_type = null;
|
|
17
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
18
|
+
if (data.select_columns) {
|
|
19
|
+
select_columns =
|
|
20
|
+
typeof data.select_columns === "string"
|
|
21
|
+
? data.select_columns.split(",").map((s) => s.trim())
|
|
22
|
+
: data.select_columns;
|
|
23
|
+
delete data.select_columns;
|
|
24
|
+
}
|
|
25
|
+
if (data.output_content_type) {
|
|
26
|
+
output_content_type = data.output_content_type;
|
|
27
|
+
delete data.output_content_type;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { select_columns, output_content_type };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Apply column projection to a single record or array of records.
|
|
35
|
+
*/
|
|
36
|
+
function applySelect(data, select_columns) {
|
|
37
|
+
if (!select_columns || select_columns.length === 0) return data;
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
return data.map((row) => pickKeys(row, select_columns));
|
|
40
|
+
}
|
|
41
|
+
if (data && typeof data === "object") {
|
|
42
|
+
return pickKeys(data, select_columns);
|
|
43
|
+
}
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function pickKeys(obj, keys) {
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const k of keys) {
|
|
50
|
+
if (obj.hasOwnProperty(k)) result[k] = obj[k];
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert data to CSV string.
|
|
57
|
+
*/
|
|
58
|
+
function toCSV(data) {
|
|
59
|
+
if (!Array.isArray(data) || data.length === 0) return "";
|
|
60
|
+
const headers = Object.keys(data[0]);
|
|
61
|
+
const rows = data.map((row) =>
|
|
62
|
+
headers
|
|
63
|
+
.map((h) => {
|
|
64
|
+
const val = row[h];
|
|
65
|
+
if (val === null || val === undefined) return "";
|
|
66
|
+
const str = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
67
|
+
return str.includes(",") || str.includes('"') || str.includes("\n")
|
|
68
|
+
? '"' + str.replace(/"/g, '""') + '"'
|
|
69
|
+
: str;
|
|
70
|
+
})
|
|
71
|
+
.join(","),
|
|
72
|
+
);
|
|
73
|
+
return headers.join(",") + "\n" + rows.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert data to simple XML string.
|
|
78
|
+
*/
|
|
79
|
+
function toXML(data, rootName = "records", itemName = "record") {
|
|
80
|
+
const items = Array.isArray(data) ? data : [data];
|
|
81
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<' + rootName + ">\n";
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
xml += " <" + itemName + ">\n";
|
|
84
|
+
for (const [key, val] of Object.entries(item)) {
|
|
85
|
+
const escaped =
|
|
86
|
+
val === null || val === undefined
|
|
87
|
+
? ""
|
|
88
|
+
: typeof val === "object"
|
|
89
|
+
? escapeXml(JSON.stringify(val))
|
|
90
|
+
: escapeXml(String(val));
|
|
91
|
+
xml += " <" + key + ">" + escaped + "</" + key + ">\n";
|
|
92
|
+
}
|
|
93
|
+
xml += " </" + itemName + ">\n";
|
|
94
|
+
}
|
|
95
|
+
xml += "</" + rootName + ">\n";
|
|
96
|
+
return xml;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function escapeXml(str) {
|
|
100
|
+
return str
|
|
101
|
+
.replace(/&/g, "&")
|
|
102
|
+
.replace(/</g, "<")
|
|
103
|
+
.replace(/>/g, ">")
|
|
104
|
+
.replace(/"/g, """);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const CREATED_AT_VARIANTS = [
|
|
108
|
+
"created_at",
|
|
109
|
+
"createdAt",
|
|
110
|
+
"created",
|
|
111
|
+
"create_date",
|
|
112
|
+
"createDate",
|
|
113
|
+
"creation_date",
|
|
114
|
+
"creationDate",
|
|
115
|
+
];
|
|
116
|
+
const MODIFIED_AT_VARIANTS = [
|
|
117
|
+
"modified_at",
|
|
118
|
+
"modifiedAt",
|
|
119
|
+
"modified",
|
|
120
|
+
"updated_at",
|
|
121
|
+
"updatedAt",
|
|
122
|
+
"updated",
|
|
123
|
+
"update_date",
|
|
124
|
+
"updateDate",
|
|
125
|
+
"modification_date",
|
|
126
|
+
"modificationDate",
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
function buildTimestampKeys(option) {
|
|
130
|
+
let createdKeys = [];
|
|
131
|
+
let modifiedKeys = [];
|
|
132
|
+
if (option.created_at) {
|
|
133
|
+
createdKeys = Array.isArray(option.created_at)
|
|
134
|
+
? option.created_at
|
|
135
|
+
: [option.created_at];
|
|
136
|
+
} else {
|
|
137
|
+
createdKeys = CREATED_AT_VARIANTS;
|
|
138
|
+
}
|
|
139
|
+
if (option.modified_at) {
|
|
140
|
+
modifiedKeys = Array.isArray(option.modified_at)
|
|
141
|
+
? option.modified_at
|
|
142
|
+
: [option.modified_at];
|
|
143
|
+
} else {
|
|
144
|
+
modifiedKeys = MODIFIED_AT_VARIANTS;
|
|
145
|
+
}
|
|
146
|
+
return { createdKeys, modifiedKeys };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function stripTimestampFields(data, keys) {
|
|
150
|
+
if (Array.isArray(data)) {
|
|
151
|
+
for (const row of data) {
|
|
152
|
+
for (const k of keys) delete row[k];
|
|
153
|
+
}
|
|
154
|
+
} else if (data && typeof data === "object") {
|
|
155
|
+
for (const k of keys) delete data[k];
|
|
156
|
+
}
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = function model(
|
|
161
|
+
dbOrTable,
|
|
162
|
+
tableOrStructure,
|
|
163
|
+
modelStructureOrPK,
|
|
164
|
+
primary_keyOrUnique,
|
|
165
|
+
uniqueOrOption,
|
|
166
|
+
optionOrUndefined,
|
|
167
|
+
) {
|
|
168
|
+
// Detect if db was passed or if first arg is the table name (string)
|
|
169
|
+
let db, table, modelStructure, primary_key, unique, option;
|
|
170
|
+
if (typeof dbOrTable === "string") {
|
|
171
|
+
// model("table", structure, pk, unique, option) — no db, use singleton
|
|
172
|
+
const restRouter = require("../index.js");
|
|
173
|
+
db = restRouter.db;
|
|
174
|
+
table = dbOrTable;
|
|
175
|
+
modelStructure = tableOrStructure || {};
|
|
176
|
+
primary_key = modelStructureOrPK || "id";
|
|
177
|
+
unique = primary_keyOrUnique || [];
|
|
178
|
+
option = uniqueOrOption || { safeDelete: null };
|
|
179
|
+
} else {
|
|
180
|
+
// model(db, "table", structure, pk, unique, option) — classic signature
|
|
181
|
+
db = dbOrTable;
|
|
182
|
+
table = tableOrStructure;
|
|
183
|
+
modelStructure = modelStructureOrPK || {};
|
|
184
|
+
primary_key = primary_keyOrUnique || "id";
|
|
185
|
+
unique = uniqueOrOption || [];
|
|
186
|
+
option = optionOrUndefined || { safeDelete: null };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { createdKeys, modifiedKeys } = buildTimestampKeys(option);
|
|
190
|
+
const allTimestampKeys = [...createdKeys, ...modifiedKeys];
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
insert: async (data) => {
|
|
194
|
+
let isBulk = false;
|
|
195
|
+
if (data.hasOwnProperty("data")) {
|
|
196
|
+
isBulk = true;
|
|
197
|
+
RemovePK(primary_key, data.data);
|
|
198
|
+
stripTimestampFields(data.data, allTimestampKeys);
|
|
199
|
+
await validateInput(
|
|
200
|
+
data,
|
|
201
|
+
getPayloadValidator("CREATE", modelStructure, primary_key, true),
|
|
202
|
+
);
|
|
203
|
+
data = data.data;
|
|
204
|
+
} else {
|
|
205
|
+
delete data[primary_key];
|
|
206
|
+
stripTimestampFields(data, allTimestampKeys);
|
|
207
|
+
await validateInput(
|
|
208
|
+
data,
|
|
209
|
+
getPayloadValidator("CREATE", modelStructure, primary_key, false),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
data = jsonStringify(data);
|
|
213
|
+
const insertResult = await db.insert(table, data, unique);
|
|
214
|
+
if (!isBulk && insertResult.hasOwnProperty("id")) {
|
|
215
|
+
const getResult = await db.get(table, [
|
|
216
|
+
[[primary_key, "=", insertResult.id]],
|
|
217
|
+
]);
|
|
218
|
+
return getResult.count > 0 ? getResult["data"][0] : null;
|
|
219
|
+
}
|
|
220
|
+
//TODO: Bulk Insert -> Return inserted objects
|
|
221
|
+
return insertResult;
|
|
222
|
+
},
|
|
223
|
+
update: async (data) => {
|
|
224
|
+
let updateResult = null;
|
|
225
|
+
if (data.hasOwnProperty("data")) {
|
|
226
|
+
stripTimestampFields(data.data, allTimestampKeys);
|
|
227
|
+
await validateInput(
|
|
228
|
+
data,
|
|
229
|
+
getPayloadValidator("UPDATE", modelStructure, primary_key, true),
|
|
230
|
+
);
|
|
231
|
+
data = data.data;
|
|
232
|
+
data = RemoveUnknownData(modelStructure, data);
|
|
233
|
+
data = jsonStringify(data);
|
|
234
|
+
updateResult = await db.upsert(table, data, unique);
|
|
235
|
+
//TODO: Bulk Update -> Return updated objects
|
|
236
|
+
} else {
|
|
237
|
+
stripTimestampFields(data, allTimestampKeys);
|
|
238
|
+
await validateInput(
|
|
239
|
+
data,
|
|
240
|
+
getPayloadValidator("UPDATE", modelStructure, primary_key, false),
|
|
241
|
+
);
|
|
242
|
+
data = RemoveUnknownData(modelStructure, [data]);
|
|
243
|
+
data = jsonStringify(data);
|
|
244
|
+
updateResult = await db.upsert(table, data, unique);
|
|
245
|
+
if (updateResult.hasOwnProperty("id") && updateResult.id > 0) {
|
|
246
|
+
const getResult = await db.get(table, [
|
|
247
|
+
[[primary_key, "=", updateResult.id]],
|
|
248
|
+
]);
|
|
249
|
+
return getResult.count > 0 ? getResult["data"][0] : null;
|
|
250
|
+
} else if (data[0].hasOwnProperty(primary_key)) {
|
|
251
|
+
const result = await db.get(
|
|
252
|
+
table,
|
|
253
|
+
[[[primary_key, "=", data[0][primary_key]]]],
|
|
254
|
+
[],
|
|
255
|
+
option.safeDelete,
|
|
256
|
+
);
|
|
257
|
+
if (result.count > 0) return result["data"][0];
|
|
258
|
+
else return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return updateResult;
|
|
262
|
+
},
|
|
263
|
+
upsert: async (data) => {
|
|
264
|
+
//* Same as Update but primary key is optional */
|
|
265
|
+
let updateResult = null;
|
|
266
|
+
if (data.hasOwnProperty("data")) {
|
|
267
|
+
stripTimestampFields(data.data, allTimestampKeys);
|
|
268
|
+
await validateInput(
|
|
269
|
+
data,
|
|
270
|
+
getPayloadValidator("CREATE", modelStructure, primary_key, true),
|
|
271
|
+
);
|
|
272
|
+
data = data.data;
|
|
273
|
+
data = RemoveUnknownData(modelStructure, data);
|
|
274
|
+
data = jsonStringify(data);
|
|
275
|
+
updateResult = await db.upsert(table, data, unique);
|
|
276
|
+
//TODO: Bulk Upsert -> Return Inserted/Updated objects
|
|
277
|
+
} else {
|
|
278
|
+
stripTimestampFields(data, allTimestampKeys);
|
|
279
|
+
await validateInput(
|
|
280
|
+
data,
|
|
281
|
+
getPayloadValidator("CREATE", modelStructure, primary_key, false),
|
|
282
|
+
);
|
|
283
|
+
data = RemoveUnknownData(modelStructure, [data]);
|
|
284
|
+
data = jsonStringify(data);
|
|
285
|
+
updateResult = await db.upsert(table, data, unique);
|
|
286
|
+
if (updateResult.hasOwnProperty("id")) {
|
|
287
|
+
const getResult = await db.get(table, [
|
|
288
|
+
[[primary_key, "=", updateResult.id]],
|
|
289
|
+
]);
|
|
290
|
+
return getResult.count > 0 ? getResult["data"][0] : null;
|
|
291
|
+
} else if (data.hasOwnProperty(primary_key)) {
|
|
292
|
+
const result = await db.get(
|
|
293
|
+
table,
|
|
294
|
+
[[[primary_key, "=", data[primary_key]]]],
|
|
295
|
+
option.safeDelete,
|
|
296
|
+
);
|
|
297
|
+
if (result.count > 0) return result["data"][0];
|
|
298
|
+
else return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return updateResult;
|
|
302
|
+
},
|
|
303
|
+
remove: async (data) => {
|
|
304
|
+
let filter = dataToFilter(jsonSafeParse(data), primary_key);
|
|
305
|
+
return await db.remove(table, filter, option.safeDelete);
|
|
306
|
+
},
|
|
307
|
+
byId: async (id, options = {}) => {
|
|
308
|
+
let type = getType(id);
|
|
309
|
+
if (type === "string" || type === "number") {
|
|
310
|
+
const result = await db.get(
|
|
311
|
+
table,
|
|
312
|
+
[[[primary_key, "=", id]]],
|
|
313
|
+
[],
|
|
314
|
+
option.safeDelete,
|
|
315
|
+
);
|
|
316
|
+
if (result.count > 0) {
|
|
317
|
+
let record = result["data"][0];
|
|
318
|
+
if (options.select_columns)
|
|
319
|
+
record = applySelect(record, options.select_columns);
|
|
320
|
+
return record;
|
|
321
|
+
} else return null;
|
|
322
|
+
} else {
|
|
323
|
+
throw new Error("Invalid id value", { cause: { status: 422 } });
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
//TODO: Implement Sort Logic
|
|
327
|
+
find: async (data) => {
|
|
328
|
+
const { select_columns } = extractReservedParams(data);
|
|
329
|
+
let sort = [];
|
|
330
|
+
if (data.hasOwnProperty("sort")) {
|
|
331
|
+
sort = data.sort;
|
|
332
|
+
delete data.sort;
|
|
333
|
+
sort = jsonSafeParse(sort);
|
|
334
|
+
}
|
|
335
|
+
let filter = dataToFilter(jsonSafeParse(data), primary_key);
|
|
336
|
+
const result = await db.get(table, filter, sort, option.safeDelete);
|
|
337
|
+
if (select_columns)
|
|
338
|
+
result.data = applySelect(result.data, select_columns);
|
|
339
|
+
return result;
|
|
340
|
+
},
|
|
341
|
+
findOne: async (data) => {
|
|
342
|
+
const { select_columns } = extractReservedParams(data);
|
|
343
|
+
let sort = [];
|
|
344
|
+
if (data.hasOwnProperty("sort")) {
|
|
345
|
+
sort = data.sort;
|
|
346
|
+
delete data.sort;
|
|
347
|
+
sort = jsonSafeParse(sort);
|
|
348
|
+
}
|
|
349
|
+
let filter = dataToFilter(jsonSafeParse(data), primary_key);
|
|
350
|
+
let result = await db.get(table, filter, sort, option.safeDelete);
|
|
351
|
+
if (result.count > 0) {
|
|
352
|
+
let record = result["data"][0];
|
|
353
|
+
if (select_columns) record = applySelect(record, select_columns);
|
|
354
|
+
return record;
|
|
355
|
+
} else {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
list: async (data) => {
|
|
360
|
+
const { select_columns } = extractReservedParams(data);
|
|
361
|
+
let page = 0;
|
|
362
|
+
let size = 30;
|
|
363
|
+
let sort = [];
|
|
364
|
+
if (data.hasOwnProperty("page")) {
|
|
365
|
+
page = parseInt(data.page, 10);
|
|
366
|
+
if (isNaN(page) || page < 0) page = 0;
|
|
367
|
+
delete data.page;
|
|
368
|
+
}
|
|
369
|
+
if (data.hasOwnProperty("size")) {
|
|
370
|
+
size = parseInt(data.size, 10);
|
|
371
|
+
if (isNaN(size) || size < 1) size = 30;
|
|
372
|
+
if (size > 200) size = 200;
|
|
373
|
+
delete data.size;
|
|
374
|
+
}
|
|
375
|
+
if (data.hasOwnProperty("sort")) {
|
|
376
|
+
sort = data.sort;
|
|
377
|
+
delete data.sort;
|
|
378
|
+
}
|
|
379
|
+
let filter = dataToFilter(jsonSafeParse(data), primary_key);
|
|
380
|
+
sort = jsonSafeParse(sort);
|
|
381
|
+
const result = await db.list(
|
|
382
|
+
table,
|
|
383
|
+
filter,
|
|
384
|
+
sort,
|
|
385
|
+
option.safeDelete,
|
|
386
|
+
page,
|
|
387
|
+
size,
|
|
388
|
+
);
|
|
389
|
+
if (select_columns)
|
|
390
|
+
result.data = applySelect(result.data, select_columns);
|
|
391
|
+
return result;
|
|
392
|
+
},
|
|
393
|
+
patch: async (data) => {
|
|
394
|
+
// Partial update — fetch existing, merge patch fields, then upsert
|
|
395
|
+
stripTimestampFields(data, allTimestampKeys);
|
|
396
|
+
if (!data.hasOwnProperty(primary_key)) {
|
|
397
|
+
throw new Error("Primary key is required for patch", {
|
|
398
|
+
cause: { status: 422 },
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const pkValue = data[primary_key];
|
|
402
|
+
// Fetch existing record
|
|
403
|
+
const existing = await db.get(
|
|
404
|
+
table,
|
|
405
|
+
[[[primary_key, "=", pkValue]]],
|
|
406
|
+
[],
|
|
407
|
+
option.safeDelete,
|
|
408
|
+
);
|
|
409
|
+
if (existing.count === 0) return null;
|
|
410
|
+
// Merge: start with existing, overlay only known fields from patch
|
|
411
|
+
const merged = { ...existing.data[0] };
|
|
412
|
+
for (const key of Object.keys(data)) {
|
|
413
|
+
if (key === primary_key) continue;
|
|
414
|
+
if (modelStructure.hasOwnProperty(key)) {
|
|
415
|
+
merged[key] = data[key];
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const pkOrig = merged[primary_key];
|
|
419
|
+
const mergedArray = jsonStringify([merged]);
|
|
420
|
+
// Restore the primary key value in case jsonStringify converted it
|
|
421
|
+
// (e.g., MongoDB ObjectId gets stringified to a JSON string)
|
|
422
|
+
mergedArray[0][primary_key] = pkOrig;
|
|
423
|
+
await db.upsert(table, mergedArray, unique);
|
|
424
|
+
// Re-fetch to return the updated record
|
|
425
|
+
const result = await db.get(
|
|
426
|
+
table,
|
|
427
|
+
[[[primary_key, "=", pkValue]]],
|
|
428
|
+
[],
|
|
429
|
+
option.safeDelete,
|
|
430
|
+
);
|
|
431
|
+
if (result.count > 0) return result["data"][0];
|
|
432
|
+
return null;
|
|
433
|
+
},
|
|
434
|
+
pk: primary_key,
|
|
435
|
+
modelStructure,
|
|
436
|
+
table,
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Export format helpers for route.js
|
|
441
|
+
module.exports.toCSV = toCSV;
|
|
442
|
+
module.exports.toXML = toXML;
|
|
443
|
+
module.exports.applySelect = applySelect;
|
|
444
|
+
module.exports.extractReservedParams = extractReservedParams;
|