masquerade-orm 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/bin/universalTsInit.js +59 -0
- package/docs/deletion.md +186 -0
- package/docs/find.md +265 -0
- package/docs/getting-started-javascript.md +112 -0
- package/docs/getting-started-typescript.md +159 -0
- package/docs/in-depth-class-definitions.md +193 -0
- package/docs/jsdoc-ux-tips.md +17 -0
- package/docs/managing-the-database.md +37 -0
- package/docs/saving-to-database.md +37 -0
- package/index.d.ts +7 -0
- package/index.js +11 -0
- package/jsconfig.json +29 -0
- package/package.json +40 -0
- package/src/ORM/DbManager.js +76 -0
- package/src/ORM/ORM.js +181 -0
- package/src/ORM/bootOrm.js +929 -0
- package/src/changeLogger/changeLogger.js +55 -0
- package/src/changeLogger/save.js +237 -0
- package/src/changeLogger/sqlClients/postgres.js +97 -0
- package/src/changeLogger/sqlClients/sqlite.js +120 -0
- package/src/entity/delete/delete.js +20 -0
- package/src/entity/delete/getDependents.js +46 -0
- package/src/entity/entity.d.ts +46 -0
- package/src/entity/entity.js +228 -0
- package/src/entity/find/find.js +157 -0
- package/src/entity/find/joins.js +52 -0
- package/src/entity/find/queryBuilder.js +95 -0
- package/src/entity/find/relations.js +23 -0
- package/src/entity/find/scopeProxies.js +60 -0
- package/src/entity/find/sqlClients/postgresFuncs.js +171 -0
- package/src/entity/find/sqlClients/sqliteFuncs.js +211 -0
- package/src/entity/find/where/relationalWhere.js +142 -0
- package/src/entity/find/where/where.js +244 -0
- package/src/entity/find/where/whereArgsFunctions.js +49 -0
- package/src/misc/classes.js +63 -0
- package/src/misc/constants.js +42 -0
- package/src/misc/miscFunctions.js +167 -0
- package/src/misc/ormStore.js +18 -0
- package/src/misc/types.d.ts +560 -0
- package/src/proxies/instanceProxy.js +544 -0
- package/src/proxies/nonRelationalArrayProxy.js +76 -0
- package/src/proxies/objectProxy.js +50 -0
- package/src/proxies/relationalArrayProxy.js +130 -0
- package/src/webpack/masquerade-loader.js +14 -0
- package/src/webpack/plugin.js +43 -0
- package/src/webpack/store.js +2 -0
- package/src/webpack/webpack.config.js +41 -0
- package/testing/dev-doc.md +7 -0
- package/testing/generationFuncs.js +21 -0
- package/testing/miscFunctions.js +97 -0
- package/testing/postgres.test.js +254 -0
- package/testing/sqlite.test.js +257 -0
- package/testing/testing-classes.js +102 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { OrmStore } from "../misc/ormStore.js"
|
|
2
|
+
import { handleRelationalChanges, handleUpserts, organizeChangeObj } from "./save.js"
|
|
3
|
+
import { postgresSaveQuery } from "./sqlClients/postgres.js"
|
|
4
|
+
import { sqliteSaveQuery } from "./sqlClients/sqlite.js"
|
|
5
|
+
import { setImmediate } from "node:timers/promises";
|
|
6
|
+
|
|
7
|
+
export class ChangeLogger {
|
|
8
|
+
static scheduledFlush = false
|
|
9
|
+
static currentlySaving = false
|
|
10
|
+
static flushChanges() {
|
|
11
|
+
const dbChangesObj = OrmStore.store.dbChangesObj
|
|
12
|
+
if (!Object.keys(dbChangesObj).length || this.scheduledFlush) return
|
|
13
|
+
this.scheduledFlush = true
|
|
14
|
+
const func = async () => await ChangeLogger.save()
|
|
15
|
+
//queueMicrotask(func)
|
|
16
|
+
setImmediate(func)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
static async save() {
|
|
21
|
+
const dbChanges = OrmStore.store.dbChangesObj
|
|
22
|
+
if (!Object.keys(dbChanges).length || ChangeLogger.currentlySaving) return
|
|
23
|
+
// ChangeLogger.currentlySaving = true
|
|
24
|
+
|
|
25
|
+
let paramIndex = 1
|
|
26
|
+
const { sqlClient, dbConnection } = OrmStore.store
|
|
27
|
+
const deletedInstancesArr = dbChanges.deletedInstancesArr
|
|
28
|
+
const deletedUncalledRelationsArr = dbChanges.deletedUncalledRelationsArr //this has to fire first
|
|
29
|
+
if (deletedInstancesArr) delete dbChanges.deletedInstancesArr
|
|
30
|
+
if (deletedUncalledRelationsArr) delete dbChanges.deletedUncalledRelationsArr
|
|
31
|
+
|
|
32
|
+
const organizedChangeObj = {}
|
|
33
|
+
organizeChangeObj(dbChanges, organizedChangeObj, sqlClient)
|
|
34
|
+
|
|
35
|
+
const entitiesChangeObj = organizedChangeObj.tables ?? {}
|
|
36
|
+
const junctionTablesChangeObj = organizedChangeObj.junctions ?? {}
|
|
37
|
+
|
|
38
|
+
const classesQueryObj = {}
|
|
39
|
+
const junctionsQueryObj = {}
|
|
40
|
+
|
|
41
|
+
for (const [tableName, classChangesObj] of Object.entries(entitiesChangeObj)) {
|
|
42
|
+
if (!Object.keys(classChangesObj).length) continue
|
|
43
|
+
paramIndex = handleUpserts(tableName, classChangesObj, classesQueryObj, paramIndex, sqlClient)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const [tableName, junctionChangesObj] of Object.entries(junctionTablesChangeObj)) {
|
|
47
|
+
if (!Object.keys(junctionChangesObj).length) continue
|
|
48
|
+
paramIndex = handleRelationalChanges(tableName, junctionChangesObj, junctionsQueryObj, paramIndex, sqlClient)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (sqlClient === "postgresql") await postgresSaveQuery(deletedUncalledRelationsArr, classesQueryObj, junctionsQueryObj, deletedInstancesArr, paramIndex, dbConnection)
|
|
52
|
+
else sqliteSaveQuery(deletedUncalledRelationsArr, classesQueryObj, junctionsQueryObj, deletedInstancesArr, dbConnection)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { newEntityInstanceSymb } from "../misc/constants.js"
|
|
2
|
+
import { coloredBackgroundConsoleLog, getPropertyClassification, jsValue2SqliteValue, nonSnake2Snake } from "../misc/miscFunctions.js"
|
|
3
|
+
import { OrmStore } from "../misc/ormStore.js"
|
|
4
|
+
import { ChangeLogger } from "./changeLogger.js"
|
|
5
|
+
import { junctionTableRemovalPostgres } from "./sqlClients/postgres.js"
|
|
6
|
+
import { junctionTableRemovalSqlite } from "./sqlClients/sqlite.js"
|
|
7
|
+
|
|
8
|
+
const idTypeSymb = Symbol(`idType`)
|
|
9
|
+
|
|
10
|
+
export function successfullSaveOperation() {
|
|
11
|
+
coloredBackgroundConsoleLog(`Save ran successfully.\n`, `success`)
|
|
12
|
+
OrmStore.clearDbChanges()
|
|
13
|
+
ChangeLogger.scheduledFlush = false
|
|
14
|
+
ChangeLogger.currentlySaving = false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function expandBuffer(arr, lengthAdded) {
|
|
18
|
+
arr.length += lengthAdded
|
|
19
|
+
return arr
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function passEntityColumnsToAncestorMaps(id, entityInstanceChangeObj, classWiki, cteMap, client) {
|
|
23
|
+
const isNewEntityInstance = entityInstanceChangeObj[newEntityInstanceSymb] ? true : false
|
|
24
|
+
cteMap.tables ??= {}
|
|
25
|
+
const baseClassCteMap = cteMap.tables[classWiki.className] ??= {}
|
|
26
|
+
baseClassCteMap[id] = { id }
|
|
27
|
+
let currentWiki = classWiki
|
|
28
|
+
while (currentWiki.parent) {
|
|
29
|
+
const classCteMap = cteMap.tables[currentWiki.parent.className] ??= {}
|
|
30
|
+
classCteMap[id] ??= { id }
|
|
31
|
+
classCteMap[id][newEntityInstanceSymb] = isNewEntityInstance
|
|
32
|
+
currentWiki = currentWiki.parent ?? currentWiki
|
|
33
|
+
}
|
|
34
|
+
cteMap.tables[currentWiki.className][id].updatedAt = client === "postgresql" ? new Date() : new Date().toISOString()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function handleRelationalChanges(tableName, tableChangesObj, queryObj, paramIndex, sqlClient) {
|
|
38
|
+
/**@type {any}*/ const target = queryObj[tableName] = {}
|
|
39
|
+
const categorizedIds = { added: {}, removed: {} }
|
|
40
|
+
const entries = Object.entries(tableChangesObj)
|
|
41
|
+
const idTypeArr = tableChangesObj[idTypeSymb]
|
|
42
|
+
|
|
43
|
+
for (const [baseEntityId, addedAndRemovedIds] of entries) {
|
|
44
|
+
if (addedAndRemovedIds.added) {
|
|
45
|
+
categorizedIds.added[baseEntityId] = []
|
|
46
|
+
for (const addedJoinId of addedAndRemovedIds.added) categorizedIds.added[baseEntityId].push(addedJoinId)
|
|
47
|
+
}
|
|
48
|
+
if (addedAndRemovedIds.removed) {
|
|
49
|
+
categorizedIds.removed[baseEntityId] = []
|
|
50
|
+
for (const removedJoinId of addedAndRemovedIds.removed) categorizedIds.removed[baseEntityId].push(removedJoinId)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let addedIds = Object.entries(categorizedIds.added)
|
|
55
|
+
let removedIds = Object.entries(categorizedIds.removed)
|
|
56
|
+
|
|
57
|
+
addedIds = addedIds.map(([joiningId, joinedIdArr]) => [typecastStringId(joiningId, idTypeArr[0]), joinedIdArr])
|
|
58
|
+
removedIds = removedIds.map(([joiningId, joinedIdArr]) => [typecastStringId(joiningId, idTypeArr[0]), joinedIdArr])
|
|
59
|
+
|
|
60
|
+
if (addedIds.length) [target.newRelationsObj, paramIndex] = junctionTableInsertion(addedIds, tableName, paramIndex, sqlClient)
|
|
61
|
+
if (removedIds.length) [target.deletedRelationsObj, paramIndex] = sqlClient === `postgresql`
|
|
62
|
+
? junctionTableRemovalPostgres(removedIds, tableName, paramIndex, idTypeArr)
|
|
63
|
+
: junctionTableRemovalSqlite(removedIds, tableName)
|
|
64
|
+
return paramIndex
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function junctionTableInsertion(addedIds, tableName, paramIndex, sqlClient) {
|
|
68
|
+
const snakedTableName = nonSnake2Snake(tableName)
|
|
69
|
+
let queryStr = `INSERT INTO ${snakedTableName} (joining_id, joined_id) VALUES `
|
|
70
|
+
const params = []
|
|
71
|
+
//const [joiningIdTypeCast, joinedIdTypeCast] = [getPostgresIdTypeCasting(idTypeArr[0]), getPostgresIdTypeCasting(idTypeArr[1])]
|
|
72
|
+
for (const baseAndJoinedIds of addedIds) {
|
|
73
|
+
const baseId = baseAndJoinedIds[0]
|
|
74
|
+
const joinedIds = baseAndJoinedIds[1]
|
|
75
|
+
while (joinedIds.length) {
|
|
76
|
+
queryStr += sqlClient === "postgresql" ? `($${paramIndex++}, $${paramIndex++}), ` : `(?, ?), `
|
|
77
|
+
params.push(baseId, joinedIds.pop())
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
queryStr = queryStr.slice(0, -2)
|
|
81
|
+
if (sqlClient === "postgresql") queryStr += ` RETURNING 1`
|
|
82
|
+
|
|
83
|
+
const returnedJunctionObj = { queryStr, params }
|
|
84
|
+
return [returnedJunctionObj, paramIndex]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
export function handleUpserts(tableName, classChangesObj, queryObj, paramIndex, sqlClient) {
|
|
89
|
+
const classInstances = Object.entries(classChangesObj)
|
|
90
|
+
const inserts = []
|
|
91
|
+
const updates = []
|
|
92
|
+
queryObj[tableName] ??= {}
|
|
93
|
+
while (classInstances.length) {
|
|
94
|
+
//@ts-ignore
|
|
95
|
+
const [instanceId, instance] = classInstances.pop()
|
|
96
|
+
if (Object.keys(instance).length === 1) continue
|
|
97
|
+
if (instance[newEntityInstanceSymb]) inserts.push(instance)
|
|
98
|
+
else updates.push(instance)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (inserts.length) paramIndex = insertNewRows(inserts, tableName, queryObj, paramIndex, sqlClient)
|
|
102
|
+
if (updates.length) paramIndex = updateRows(updates, tableName, queryObj, paramIndex, sqlClient)
|
|
103
|
+
return paramIndex
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function typecastStringId(instanceId, idType) {
|
|
107
|
+
if (idType === `number`) return parseInt(instanceId, 10)
|
|
108
|
+
return instanceId
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function insertNewRows(newRows, tableName, queryObj, paramIndex, client) {
|
|
112
|
+
/**@type {any}*/ const target = queryObj[tableName].insert = { queryStr: ``, params: [] }
|
|
113
|
+
const snakedTableName = nonSnake2Snake(tableName)
|
|
114
|
+
const classWiki = OrmStore.store.classWikiDict[tableName]
|
|
115
|
+
const columns = Object.keys(classWiki.columns)
|
|
116
|
+
|
|
117
|
+
if (classWiki.parent) queryObj[tableName].parent = classWiki.parent.className
|
|
118
|
+
|
|
119
|
+
target.params = expandBuffer(target.params, newRows.length * columns.length)
|
|
120
|
+
let i = 0
|
|
121
|
+
let queryStr = `INSERT INTO ${snakedTableName} (${columns.map(column => nonSnake2Snake(column)).join(', ')}) VALUES `
|
|
122
|
+
|
|
123
|
+
for (const instance of newRows) {
|
|
124
|
+
if (client === "postgresql") queryStr += `(${columns.map(column => `$${paramIndex++}`).join(', ')}), \n`
|
|
125
|
+
else queryStr += `(${columns.map(column => `?`).join(', ')}), \n`
|
|
126
|
+
|
|
127
|
+
for (const column of columns) target.params[i++] = instance[column]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (client === "postgresql") target.queryStr = queryStr.slice(0, -3) + ` RETURNING 1`
|
|
131
|
+
else target.queryStr = queryStr.slice(0, -3)
|
|
132
|
+
return paramIndex
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function updateRows(updatedRows, tableName, queryObj, paramIndex, client) {
|
|
136
|
+
/**@type {any}*/ const target = queryObj[tableName].update = { queryStrArr: [], params2dArr: [] }
|
|
137
|
+
const snakedTableName = nonSnake2Snake(tableName)
|
|
138
|
+
const idType = OrmStore.getClassWiki(tableName).columns.id.type
|
|
139
|
+
|
|
140
|
+
for (const row of updatedRows) {
|
|
141
|
+
let rowId = row.id
|
|
142
|
+
delete row.id
|
|
143
|
+
const updatedColumns = Object.entries(row)
|
|
144
|
+
//if (!updatedColumns.length) continue
|
|
145
|
+
rowId = typecastStringId(rowId, idType)
|
|
146
|
+
let queryStr = ``
|
|
147
|
+
let params = []
|
|
148
|
+
|
|
149
|
+
queryStr += `UPDATE ${snakedTableName} SET `
|
|
150
|
+
for (const [columnName, val] of updatedColumns) {
|
|
151
|
+
if (client === "postgresql") queryStr += `${nonSnake2Snake(columnName)} = $${paramIndex++}, `
|
|
152
|
+
else queryStr += `${nonSnake2Snake(columnName)} = ?, `
|
|
153
|
+
|
|
154
|
+
params.push(val)
|
|
155
|
+
}
|
|
156
|
+
if (client === "postgresql") queryStr = queryStr.slice(0, -2) + ` WHERE id = $${paramIndex++} RETURNING 1`
|
|
157
|
+
else queryStr = queryStr.slice(0, -2) + ` WHERE id = ?`
|
|
158
|
+
|
|
159
|
+
params.push(rowId)
|
|
160
|
+
target.queryStrArr.push(queryStr)
|
|
161
|
+
target.params2dArr.push(params)
|
|
162
|
+
}
|
|
163
|
+
return paramIndex
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function organizeChangeObj(dbChanges, cteMap, client) {
|
|
167
|
+
const classNames = Object.keys(dbChanges)
|
|
168
|
+
|
|
169
|
+
for (const className of classNames) {
|
|
170
|
+
const tableChangeObj = dbChanges[className]
|
|
171
|
+
const entityChangeObjects = Object.entries(tableChangeObj)
|
|
172
|
+
|
|
173
|
+
for (const [instanceId, entityInstanceChangeObj] of entityChangeObjects) {
|
|
174
|
+
|
|
175
|
+
const classWiki = OrmStore.store.classWikiDict[className]
|
|
176
|
+
let properties = Object.keys(entityInstanceChangeObj)
|
|
177
|
+
|
|
178
|
+
if (classWiki.parent) {
|
|
179
|
+
passEntityColumnsToAncestorMaps(instanceId, entityInstanceChangeObj, classWiki, cteMap, client)
|
|
180
|
+
properties = properties.filter(prop => prop !== "id" && prop !== "updatedAt")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const property of properties) {
|
|
184
|
+
const [classification, columnType, mapWithProp] = getPropertyClassification(property, classWiki)
|
|
185
|
+
|
|
186
|
+
if (classification === "Join" || classification === "ParentJoin") {
|
|
187
|
+
const added = entityInstanceChangeObj[property].added
|
|
188
|
+
const removed = entityInstanceChangeObj[property].removed
|
|
189
|
+
if (!added.length && !removed.length) continue
|
|
190
|
+
|
|
191
|
+
const junctionTableName = `${nonSnake2Snake(mapWithProp.className)}___${nonSnake2Snake(property)}_jt`
|
|
192
|
+
|
|
193
|
+
cteMap.junctions ??= {}
|
|
194
|
+
const tableCteMap = cteMap.junctions[junctionTableName] ??= {}
|
|
195
|
+
tableCteMap[idTypeSymb] = [mapWithProp.columns.id.type, columnType.columns.id.type]
|
|
196
|
+
|
|
197
|
+
if (added.length) {
|
|
198
|
+
tableCteMap[instanceId] ??= {}
|
|
199
|
+
tableCteMap[instanceId].added = entityInstanceChangeObj[property].added
|
|
200
|
+
}
|
|
201
|
+
if (removed.length) {
|
|
202
|
+
tableCteMap[instanceId] ??= {}
|
|
203
|
+
tableCteMap[instanceId].removed = entityInstanceChangeObj[property].removed
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const tableName = mapWithProp.className
|
|
208
|
+
cteMap.tables ??= {}
|
|
209
|
+
const tableCteMap = cteMap.tables[tableName] ??= {}
|
|
210
|
+
const entityInstanceMap = tableCteMap[instanceId] ??= {}
|
|
211
|
+
//tableCteMap[idTypeSymb] = [mapWithProp.columns.id.type, columnType.type]
|
|
212
|
+
|
|
213
|
+
if (client === "postgresql") entityInstanceMap[property] = entityInstanceChangeObj[property]
|
|
214
|
+
else {
|
|
215
|
+
const value = entityInstanceChangeObj[property]
|
|
216
|
+
entityInstanceMap[property] = jsValue2SqliteValue(value)
|
|
217
|
+
}
|
|
218
|
+
entityInstanceMap[newEntityInstanceSymb] = entityInstanceChangeObj[newEntityInstanceSymb] ? true : false
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
// function getPostgresTypeCasting(columnObj) {
|
|
227
|
+
// const { type, isArray } = columnObj
|
|
228
|
+
// let returnedType
|
|
229
|
+
// if (type === `string`) returnedType = `::text`
|
|
230
|
+
// else if (type === `number`) returnedType = `::int`
|
|
231
|
+
// else if (type === `bigint`) returnedType = `::bigint`
|
|
232
|
+
// else if (type === `Date`) returnedType = `::timestamptz`
|
|
233
|
+
// else if (type === `boolean`) returnedType = `::bool`
|
|
234
|
+
// else if (type === `object`) returnedType = `::jsonb`
|
|
235
|
+
// if (isArray && type !== `object`) returnedType += `[]`
|
|
236
|
+
// return returnedType
|
|
237
|
+
// }
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { coloredBackgroundConsoleLog, nonSnake2Snake } from "../../misc/miscFunctions.js"
|
|
2
|
+
import { successfullSaveOperation } from "../save.js"
|
|
3
|
+
|
|
4
|
+
export async function postgresSaveQuery(deletedUncalledRelationsArr, classesQueryObj, junctionsQueryObj, deletedInstancesArr, paramIndex, dbConnection) {
|
|
5
|
+
let i = 0
|
|
6
|
+
let finalString = 'WITH'
|
|
7
|
+
const finalParams = []
|
|
8
|
+
paramIndex = 1
|
|
9
|
+
|
|
10
|
+
let queryFunc = dbConnection.query.bind(dbConnection)
|
|
11
|
+
let queryFuncWithTryCatch = async (query, params, forDeletedUncalledRelations = false) => {
|
|
12
|
+
try {
|
|
13
|
+
await queryFunc(query, params)
|
|
14
|
+
if (!forDeletedUncalledRelations) successfullSaveOperation()
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
coloredBackgroundConsoleLog(`Database save failed. ${e}\n`, `failure`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (deletedUncalledRelationsArr) {
|
|
22
|
+
const paramsArr = []
|
|
23
|
+
let finalString = 'WITH'
|
|
24
|
+
for (const [tableName, { idType, params }] of Object.entries(deletedUncalledRelationsArr)) {
|
|
25
|
+
const queryStr = `DELETE FROM ${tableName} WHERE joining_id = ANY($${paramIndex++}::${idType}[])`
|
|
26
|
+
finalString += ` cte${i++} AS (` + queryStr + `), `
|
|
27
|
+
paramsArr.push(params)
|
|
28
|
+
}
|
|
29
|
+
await queryFuncWithTryCatch(finalString.slice(0, -2) + ` SELECT 1`, paramsArr, true)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const upsertObj of Object.values(classesQueryObj)) {
|
|
33
|
+
if (upsertObj.insert) {
|
|
34
|
+
finalString += ` cte${i++} AS (` + upsertObj.insert.queryStr + `), `
|
|
35
|
+
finalParams.push(...upsertObj.insert.params)
|
|
36
|
+
}
|
|
37
|
+
if (upsertObj.update) {
|
|
38
|
+
finalString += upsertObj.update.queryStrArr.map((queryStr) => ` cte${i++} AS (${queryStr})`).join(', ') + `, `
|
|
39
|
+
finalParams.push(...upsertObj.update.params2dArr.flat())
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const junctionObj of Object.values(junctionsQueryObj)) {
|
|
44
|
+
if (junctionObj.newRelationsObj) {
|
|
45
|
+
finalString += ` cte${i++} AS (` + junctionObj.newRelationsObj.queryStr + `), `
|
|
46
|
+
finalParams.push(...junctionObj.newRelationsObj.params)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (junctionObj.deletedRelationsObj) {
|
|
50
|
+
finalString += ` cte${i++} AS (` + junctionObj.deletedRelationsObj.queryStr + `), `
|
|
51
|
+
finalParams.push(...junctionObj.deletedRelationsObj.params)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (deletedInstancesArr) {
|
|
56
|
+
paramIndex = finalParams.length + 1
|
|
57
|
+
for (const [tableName, param] of deletedInstancesArr) {
|
|
58
|
+
const queryStr = `DELETE FROM ${tableName} WHERE id = $${paramIndex++}`
|
|
59
|
+
finalString += ` cte${i++} AS (` + queryStr + `), `
|
|
60
|
+
finalParams.push(param)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
finalString = finalString.slice(0, -2) + ` SELECT 1`
|
|
65
|
+
await queryFuncWithTryCatch(finalString, finalParams)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
function getPostgresIdTypeCasting(idType) {
|
|
70
|
+
if (idType === `string`) return `::uuid`
|
|
71
|
+
else if (idType === `number`) return `::int`
|
|
72
|
+
else return `::bigint`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export function junctionTableRemovalPostgres(removedIds, tableName, paramIndex, idTypeArr) {
|
|
77
|
+
const snakedTableName = nonSnake2Snake(tableName)
|
|
78
|
+
let queryStr = ``
|
|
79
|
+
const params = []
|
|
80
|
+
|
|
81
|
+
const [joiningIdTypeCast, joinedIdTypeCast] = [getPostgresIdTypeCasting(idTypeArr[0]), getPostgresIdTypeCasting(idTypeArr[1])]
|
|
82
|
+
queryStr = `DELETE FROM ${snakedTableName} AS jt USING (VALUES`
|
|
83
|
+
for (const idPairings of removedIds) {
|
|
84
|
+
const joiningId = idPairings[0]
|
|
85
|
+
const removedJoinedIds = idPairings[1]
|
|
86
|
+
for (const removedId of removedJoinedIds) {
|
|
87
|
+
queryStr += `($${paramIndex++}${joiningIdTypeCast}, $${paramIndex++}${joinedIdTypeCast}), `
|
|
88
|
+
params.push(joiningId, removedId)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
queryStr = queryStr.slice(0, -2) + `) AS to_delete(joining_id, joined_id) `
|
|
92
|
+
queryStr += `WHERE jt.joining_id = to_delete.joining_id AND jt.joined_id = to_delete.joined_id `
|
|
93
|
+
queryStr += `RETURNING 1`
|
|
94
|
+
|
|
95
|
+
const returnedJunctionObj = { queryStr, params }
|
|
96
|
+
return [returnedJunctionObj, paramIndex]
|
|
97
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { coloredBackgroundConsoleLog, nonSnake2Snake } from "../../misc/miscFunctions.js"
|
|
2
|
+
import { successfullSaveOperation } from "../save.js"
|
|
3
|
+
|
|
4
|
+
export function sqliteSaveQuery(deletedUncalledRelationsArr, classesQueryObj, junctionsQueryObj, deletedInstancesArr, dbConnection) {
|
|
5
|
+
const classTableNames = Object.keys(classesQueryObj ?? {})
|
|
6
|
+
const queryFuncWithTryCatch = (queryStr, params) => {
|
|
7
|
+
try {
|
|
8
|
+
const queryFunc = dbConnection.prepare(queryStr)
|
|
9
|
+
queryFunc.run(...params)
|
|
10
|
+
return true
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
coloredBackgroundConsoleLog(`Database save failed. ${e}\n`, `failure`)
|
|
14
|
+
dbConnection.exec('ROLLBACK;')
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let visitedTables = []
|
|
20
|
+
dbConnection.exec('BEGIN;')
|
|
21
|
+
|
|
22
|
+
if (deletedUncalledRelationsArr) {
|
|
23
|
+
for (const [tableName, idArr] of Object.entries(deletedUncalledRelationsArr)) {
|
|
24
|
+
const queryStr = `DELETE FROM ${tableName} WHERE joining_id IN (${idArr.map(id => `?`).join(`, `)})`
|
|
25
|
+
if (!queryFuncWithTryCatch(queryStr, idArr)) return
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const tableName of classTableNames) {
|
|
30
|
+
if (visitedTables.includes(tableName)) continue
|
|
31
|
+
const queryObj = classesQueryObj[tableName]
|
|
32
|
+
|
|
33
|
+
visitedTables = queryObj.parent
|
|
34
|
+
? sqliteHandleAncestry(tableName, queryObj, visitedTables, classesQueryObj, queryFuncWithTryCatch)
|
|
35
|
+
: queryQueryObj(queryObj, tableName, visitedTables, queryFuncWithTryCatch)
|
|
36
|
+
if (!visitedTables) return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const junctionTableQueryObjects = Object.values(junctionsQueryObj ?? {})
|
|
40
|
+
for (const queryObj of junctionTableQueryObjects) {
|
|
41
|
+
if (queryObj.deletedRelationsObj)
|
|
42
|
+
if (!queryFuncWithTryCatch(queryObj.deletedRelationsObj.queryStr, queryObj.deletedRelationsObj.params)) return
|
|
43
|
+
|
|
44
|
+
if (queryObj.newRelationsObj)
|
|
45
|
+
if (!queryFuncWithTryCatch(queryObj.newRelationsObj.queryStr, queryObj.newRelationsObj.params)) return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (deletedInstancesArr) {
|
|
49
|
+
for (const [tableName, param] of deletedInstancesArr) {
|
|
50
|
+
const queryStr = `DELETE FROM ${tableName} WHERE id = ?;`
|
|
51
|
+
if (!queryFuncWithTryCatch(queryStr, [param])) return
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
dbConnection.exec('COMMIT;')
|
|
56
|
+
successfullSaveOperation()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
function queryQueryObj(queryObj, tableName, visitedTables, queryFuncWithTryCatch) {
|
|
61
|
+
visitedTables.push(tableName)
|
|
62
|
+
if (queryObj.insert) {
|
|
63
|
+
if (!queryFuncWithTryCatch(queryObj.insert.queryStr, queryObj.insert.params)) return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (queryObj.update) {
|
|
67
|
+
for (const [index, queryStr] of Object.entries(queryObj.update.queryStrArr))
|
|
68
|
+
if (!queryFuncWithTryCatch(queryStr, queryObj.update.params2dArr[index])) return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return visitedTables
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sqliteHandleAncestry(tableName, queryObj, visitedTables, classesQueryObj, queryFuncWithTryCatch) {
|
|
75
|
+
if (visitedTables.includes(queryObj.parent)) {
|
|
76
|
+
const queryRes = queryQueryObj(queryObj, tableName, visitedTables, queryFuncWithTryCatch)
|
|
77
|
+
if (queryRes) {
|
|
78
|
+
visitedTables = queryRes
|
|
79
|
+
return visitedTables
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const ancestryArr = [tableName]
|
|
85
|
+
let currentQueryObj = queryObj
|
|
86
|
+
while (currentQueryObj.parent) {
|
|
87
|
+
const parentName = currentQueryObj.parent
|
|
88
|
+
ancestryArr.push(parentName)
|
|
89
|
+
currentQueryObj = classesQueryObj[parentName]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
while (ancestryArr.length) {
|
|
93
|
+
const ancestorTableName = ancestryArr.pop()
|
|
94
|
+
currentQueryObj = classesQueryObj[ancestorTableName]
|
|
95
|
+
const queryRes = queryQueryObj(currentQueryObj, ancestorTableName, visitedTables, queryFuncWithTryCatch)
|
|
96
|
+
if (queryRes) visitedTables = queryRes
|
|
97
|
+
else return false
|
|
98
|
+
}
|
|
99
|
+
return visitedTables
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
export function junctionTableRemovalSqlite(removedIds, tableName) {
|
|
104
|
+
const snakedTableName = nonSnake2Snake(tableName)
|
|
105
|
+
let queryStr = ``
|
|
106
|
+
const params = []
|
|
107
|
+
|
|
108
|
+
queryStr = `DELETE FROM ${snakedTableName} WHERE (joining_id, joined_id) IN (`
|
|
109
|
+
for (const idPairings of removedIds) {
|
|
110
|
+
const baseId = idPairings[0]
|
|
111
|
+
const nonBaseIds = idPairings[1]
|
|
112
|
+
for (const removedId of nonBaseIds) {
|
|
113
|
+
queryStr += `(?, ?), `
|
|
114
|
+
params.push(baseId, removedId)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
queryStr = queryStr.slice(0, -2) + `)`
|
|
118
|
+
const returnedJunctionObj = { queryStr, params }
|
|
119
|
+
return [returnedJunctionObj, undefined]
|
|
120
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function throwDeletionErr(className, id4Deletion) {
|
|
2
|
+
throw new Error(`Unable to delete ${className} with id of ${id4Deletion} due to potential relational dependencies. Use the async method 'getDependents' to get relational dependency data and decouple it from the deleted instance for the deletion to work.`)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function throwImproperDecouplingErr(className, id4Deletion) {
|
|
6
|
+
throw new Error(`Unable to delete ${className} with id of ${id4Deletion} due to incomplete decoupling.`)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function validateDependentDataDecoupling(dependentsData, id4deletion) {
|
|
11
|
+
for (const dependency2dArr of Object.values(dependentsData)) {
|
|
12
|
+
const [dependentInstanceArr, props2Check] = dependency2dArr
|
|
13
|
+
for (const instance of dependentInstanceArr) {
|
|
14
|
+
for (const prop of props2Check) {
|
|
15
|
+
if (instance[prop].id === id4deletion) return false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { OrmStore } from "../../misc/ormStore.js"
|
|
2
|
+
import { DependentsFinalizationRegistry, ORM } from "../../ORM/ORM.js"
|
|
3
|
+
import { aliasedFindWiki2QueryRes, parseFindWiki } from "../find/find.js"
|
|
4
|
+
import { deproxifyScopeProxy, classWiki2ScopeProxy } from "../find/scopeProxies.js"
|
|
5
|
+
import { postgresCreateProxyArray } from "../find/sqlClients/postgresFuncs.js"
|
|
6
|
+
import { sqliteCreateProxyArray } from "../find/sqlClients/sqliteFuncs.js"
|
|
7
|
+
|
|
8
|
+
export async function internalFind(dependentMap, relationalProps, searchedId) {
|
|
9
|
+
const { sqlClient, dbConnection, entities } = OrmStore.store
|
|
10
|
+
const baseProxyMap = classWiki2ScopeProxy({ ...dependentMap })
|
|
11
|
+
let findWiki = deproxifyScopeProxy(baseProxyMap)
|
|
12
|
+
const eagerLoadObj = {}
|
|
13
|
+
for (const prop of relationalProps) internalFindSetup(prop, findWiki, eagerLoadObj, searchedId)
|
|
14
|
+
|
|
15
|
+
const [aliasedFindMap, joinStatements, whereObj] = parseFindWiki(findWiki)
|
|
16
|
+
const [queryResult, eagerLoadMap] = await aliasedFindWiki2QueryRes(aliasedFindMap, joinStatements, whereObj, eagerLoadObj, dependentMap, dbConnection, true)
|
|
17
|
+
const instanceArr = sqlClient === "postgresql" ?
|
|
18
|
+
postgresCreateProxyArray(queryResult, eagerLoadMap, entities, eagerLoadObj) :
|
|
19
|
+
sqliteCreateProxyArray(queryResult, eagerLoadMap, entities, true)
|
|
20
|
+
|
|
21
|
+
return instanceArr
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export function insertDependentsData(className, dependedOnId, dependentsData, dependentsMapsObj) {
|
|
26
|
+
const map = dependentsMapsObj[className] ??= new Map()
|
|
27
|
+
map.set(dependedOnId, new WeakRef(dependentsData))
|
|
28
|
+
ORM[DependentsFinalizationRegistry].register(dependentsData, [className, dependedOnId])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export function internalFindSetup(prop, findWiki, eagerLoadObj, searchedId) {
|
|
34
|
+
const { [prop]: relation, ...restOfRelations } = findWiki.uncalledJunctions_
|
|
35
|
+
const relationCopy = { ...relation }
|
|
36
|
+
relationCopy.where = { id: searchedId }
|
|
37
|
+
for (const key of Object.keys(relationCopy)) {
|
|
38
|
+
relationCopy[key + `_`] = relationCopy[key]
|
|
39
|
+
delete relationCopy[key]
|
|
40
|
+
}
|
|
41
|
+
const { junctions_, ...noJunctionsRelation } = relationCopy
|
|
42
|
+
findWiki.uncalledJunctions_ = restOfRelations
|
|
43
|
+
findWiki.junctions_ ??= {}
|
|
44
|
+
findWiki.junctions_[prop] = noJunctionsRelation
|
|
45
|
+
eagerLoadObj[prop] = true
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
import type { FindObj } from "../misc/types"
|
|
3
|
+
|
|
4
|
+
export declare class Entity {
|
|
5
|
+
id: string | number | bigint
|
|
6
|
+
updatedAt: Date
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Finds instances in the database that match the given argument.
|
|
10
|
+
* Relations do not get filtered by where conditions, only the root instances get filtered.
|
|
11
|
+
* (RootClass.find(arg)) => only RootClass instances matching the where conditions get returned.
|
|
12
|
+
*/
|
|
13
|
+
static find<T extends Entity>(
|
|
14
|
+
this: new (...args: any[]) => T,
|
|
15
|
+
obj: FindObj<T>
|
|
16
|
+
): Promise<T[]>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hard deletes the instance from the database. May require a pre-deletion step - the 'getDependents' method.
|
|
20
|
+
*/
|
|
21
|
+
delete(): Promise<void>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A pre-deletion step that is required in certain cases.
|
|
25
|
+
* Finds all instances that have a one-to-one relationship with the calling instance
|
|
26
|
+
* where the related property cannot be set to `undefined`.
|
|
27
|
+
* These relationships must be reassigned before the calling instance
|
|
28
|
+
* can be safely deleted.
|
|
29
|
+
* Returns undefined if there are no dependents.
|
|
30
|
+
*/
|
|
31
|
+
getDependents(): Promise<DependentsDict | undefined>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds all instances that have a relation with the calling instance,
|
|
35
|
+
* This method is a superset of the getDependents method, and is not meant as a pre-deletion step, but as a utility.
|
|
36
|
+
*/
|
|
37
|
+
getReferencers(): Promise<DependentsDict | undefined>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
type DependentsDict = {
|
|
42
|
+
[key: string]: [
|
|
43
|
+
dependentInstances: any[],
|
|
44
|
+
dependentProps: string[]
|
|
45
|
+
]
|
|
46
|
+
}
|