json-rest-api 1.0.21 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +4 -4
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -1,19 +1,473 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file Creates Knex table definitions from json-rest-schema definitions
|
|
2
|
+
* @file Creates Knex table definitions and migration plans from json-rest-schema definitions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
import { buildStorageInfo } from './storage/storage-mapping.js'
|
|
6
|
+
|
|
7
|
+
const MYSQL_DIALECT_PATTERN = /mysql/i
|
|
8
|
+
|
|
9
|
+
function normalizeText (value) {
|
|
10
|
+
if (value == null) return ''
|
|
11
|
+
return String(value).trim()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function quoteJsString (value) {
|
|
15
|
+
const escaped = String(value)
|
|
16
|
+
.replace(/\\/g, '\\\\')
|
|
17
|
+
.replace(/'/g, "\\'")
|
|
18
|
+
.replace(/\r/g, '\\r')
|
|
19
|
+
.replace(/\n/g, '\\n')
|
|
20
|
+
|
|
21
|
+
return `'${escaped}'`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatArrayLiteral (values = []) {
|
|
25
|
+
return `[${values.map((value) => quoteJsString(value)).join(', ')}]`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatPlainObjectLiteral (value) {
|
|
29
|
+
const entries = Object.entries(value).map(([key, entryValue]) => {
|
|
30
|
+
return `${JSON.stringify(key)}: ${formatCodeLiteral(entryValue)}`
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return `{ ${entries.join(', ')} }`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatCodeLiteral (value) {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
throw new Error('Cannot generate a migration literal from undefined.')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof value === 'function') {
|
|
42
|
+
throw new Error('Cannot generate deterministic migrations for function defaults.')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
return quoteJsString(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof value === 'number' || typeof value === 'bigint') {
|
|
50
|
+
return String(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value === 'boolean') {
|
|
54
|
+
return value ? 'true' : 'false'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (value === null) {
|
|
58
|
+
return 'null'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return `[${value.map((entry) => formatCodeLiteral(entry)).join(', ')}]`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
return formatPlainObjectLiteral(value)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return JSON.stringify(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectKnexDialect (knex) {
|
|
73
|
+
return normalizeText(
|
|
74
|
+
knex?.client?.config?.client ||
|
|
75
|
+
knex?.client?.dialect ||
|
|
76
|
+
knex?.client?.driverName
|
|
77
|
+
).toLowerCase()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isMysqlDialect (dialect = '') {
|
|
81
|
+
return MYSQL_DIALECT_PATTERN.test(String(dialect || ''))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ensureStringArray (value, label) {
|
|
85
|
+
const values = Array.isArray(value) ? value : [value]
|
|
86
|
+
const normalized = values
|
|
87
|
+
.map((entry) => normalizeText(entry))
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
|
|
90
|
+
if (normalized.length < 1) {
|
|
91
|
+
throw new Error(`${label} must define at least one column.`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return normalized
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSetColumnType (values = []) {
|
|
98
|
+
const normalized = values.map((value) => normalizeText(value)).filter(Boolean)
|
|
99
|
+
if (normalized.length < 1) {
|
|
100
|
+
throw new Error('setValues must contain at least one value.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return `set(${normalized.map((value) => quoteJsString(value)).join(', ')})`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createTableBuilderContext (schemaStructure = {}, idColumn = 'id') {
|
|
107
|
+
return {
|
|
108
|
+
schemaStructure,
|
|
109
|
+
idProperty: idColumn,
|
|
110
|
+
storageInfo: buildStorageInfo({
|
|
111
|
+
schemaStructure,
|
|
112
|
+
idProperty: idColumn
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveFieldOrColumnName (tableContext, fieldOrColumn) {
|
|
118
|
+
const normalized = normalizeText(fieldOrColumn)
|
|
119
|
+
if (!normalized) return ''
|
|
120
|
+
|
|
121
|
+
if (normalized === 'id') {
|
|
122
|
+
return tableContext.storageInfo.idColumn
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return tableContext.storageInfo.fields[normalized]?.column || normalized
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getColumnNameForDefinition (tableContext, fieldName) {
|
|
129
|
+
return resolveFieldOrColumnName(tableContext, fieldName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function normalizeTopLevelIndexes (schemaLike, tableName, tableContext) {
|
|
133
|
+
const indexes = Array.isArray(schemaLike?.indexes) ? schemaLike.indexes : []
|
|
14
134
|
|
|
15
|
-
|
|
135
|
+
return indexes.map((indexDef, indexPosition) => {
|
|
136
|
+
if (!indexDef || typeof indexDef !== 'object' || Array.isArray(indexDef)) {
|
|
137
|
+
throw new Error(`Index metadata for table '${tableName}' must be an object.`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const columns = ensureStringArray(indexDef.columns, `Index ${indexPosition + 1}`)
|
|
141
|
+
.map((column) => resolveFieldOrColumnName(tableContext, column))
|
|
142
|
+
const unique = indexDef.unique === true
|
|
143
|
+
const indexType = normalizeText(indexDef.indexType).toUpperCase()
|
|
144
|
+
const name = normalizeText(indexDef.name) || `${unique ? 'uq' : 'idx'}_${tableName}_${columns.join('_')}`
|
|
145
|
+
|
|
146
|
+
if (unique && indexType) {
|
|
147
|
+
throw new Error(`Unique index '${name}' on table '${tableName}' cannot define indexType.`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
unique,
|
|
153
|
+
columns,
|
|
154
|
+
indexType
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeFieldIndexes (schemaStructure, tableName, tableContext) {
|
|
160
|
+
const indexes = []
|
|
161
|
+
|
|
162
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
163
|
+
const columnName = getColumnNameForDefinition(tableContext, fieldName)
|
|
164
|
+
|
|
165
|
+
if (definition.unique === true) {
|
|
166
|
+
indexes.push({
|
|
167
|
+
name: `uq_${tableName}_${columnName}`,
|
|
168
|
+
unique: true,
|
|
169
|
+
columns: [columnName],
|
|
170
|
+
indexType: ''
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (definition.index === true) {
|
|
175
|
+
indexes.push({
|
|
176
|
+
name: `idx_${tableName}_${columnName}`,
|
|
177
|
+
unique: false,
|
|
178
|
+
columns: [columnName],
|
|
179
|
+
indexType: ''
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return indexes
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeTopLevelForeignKeys (schemaLike, tableName, tableContext) {
|
|
188
|
+
const foreignKeys = Array.isArray(schemaLike?.foreignKeys) ? schemaLike.foreignKeys : []
|
|
189
|
+
|
|
190
|
+
return foreignKeys.map((foreignKeyDef, foreignKeyPosition) => {
|
|
191
|
+
if (!foreignKeyDef || typeof foreignKeyDef !== 'object' || Array.isArray(foreignKeyDef)) {
|
|
192
|
+
throw new Error(`Foreign key metadata for table '${tableName}' must be an object.`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const columns = ensureStringArray(foreignKeyDef.columns, `Foreign key ${foreignKeyPosition + 1}`)
|
|
196
|
+
.map((column) => resolveFieldOrColumnName(tableContext, column))
|
|
197
|
+
const referencedTableName = normalizeText(
|
|
198
|
+
foreignKeyDef.referencedTableName ||
|
|
199
|
+
foreignKeyDef.table ||
|
|
200
|
+
foreignKeyDef.references?.table
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if (!referencedTableName) {
|
|
204
|
+
throw new Error(`Foreign key ${foreignKeyPosition + 1} on table '${tableName}' requires referencedTableName.`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const referencedColumns = ensureStringArray(
|
|
208
|
+
foreignKeyDef.referencedColumns ||
|
|
209
|
+
foreignKeyDef.references?.columns ||
|
|
210
|
+
foreignKeyDef.references?.column ||
|
|
211
|
+
'id',
|
|
212
|
+
`Foreign key ${foreignKeyPosition + 1}`
|
|
213
|
+
)
|
|
214
|
+
const name = normalizeText(foreignKeyDef.name) || `${tableName}_${columns.join('_')}_foreign`
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name,
|
|
218
|
+
columns,
|
|
219
|
+
referencedTableName,
|
|
220
|
+
referencedColumns,
|
|
221
|
+
deleteRule: normalizeText(foreignKeyDef.deleteRule || foreignKeyDef.onDelete).toUpperCase(),
|
|
222
|
+
updateRule: normalizeText(foreignKeyDef.updateRule || foreignKeyDef.onUpdate).toUpperCase()
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeFieldForeignKeys (schemaStructure, tableName, tableContext) {
|
|
228
|
+
const foreignKeys = []
|
|
229
|
+
|
|
230
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
231
|
+
if (!definition.references) continue
|
|
232
|
+
|
|
233
|
+
const columnName = getColumnNameForDefinition(tableContext, fieldName)
|
|
234
|
+
foreignKeys.push({
|
|
235
|
+
name: normalizeText(definition.references.name) || `${tableName}_${columnName}_foreign`,
|
|
236
|
+
columns: [columnName],
|
|
237
|
+
referencedTableName: normalizeText(definition.references.table),
|
|
238
|
+
referencedColumns: [normalizeText(definition.references.column || 'id')],
|
|
239
|
+
deleteRule: normalizeText(definition.references.onDelete).toUpperCase(),
|
|
240
|
+
updateRule: normalizeText(definition.references.onUpdate).toUpperCase()
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return foreignKeys
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeCheckConstraints (schemaLike, tableName) {
|
|
248
|
+
const constraints = Array.isArray(schemaLike?.checkConstraints) ? schemaLike.checkConstraints : []
|
|
249
|
+
|
|
250
|
+
return constraints.map((constraintDef, constraintIndex) => {
|
|
251
|
+
if (!constraintDef || typeof constraintDef !== 'object' || Array.isArray(constraintDef)) {
|
|
252
|
+
throw new Error(`Check constraint metadata for table '${tableName}' must be an object.`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const clause = normalizeText(constraintDef.clause)
|
|
256
|
+
if (!clause) {
|
|
257
|
+
throw new Error(`Check constraint ${constraintIndex + 1} on table '${tableName}' requires a clause.`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
name: normalizeText(constraintDef.name) || `${tableName}_check_${constraintIndex + 1}`,
|
|
262
|
+
clause
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function dedupeBySignature (entries, signatureFn, sortFn) {
|
|
268
|
+
const deduped = new Map()
|
|
269
|
+
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
deduped.set(signatureFn(entry), entry)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return [...deduped.values()].sort(sortFn)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function compareByName (left, right) {
|
|
278
|
+
return left.name.localeCompare(right.name)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeColumnShape (definition = {}) {
|
|
282
|
+
if (Array.isArray(definition.enum) && definition.enum.length > 0) {
|
|
283
|
+
return 'enum'
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(definition.setValues) && definition.setValues.length > 0) {
|
|
286
|
+
return 'set'
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
switch (definition.type) {
|
|
290
|
+
case 'id':
|
|
291
|
+
return 'integer'
|
|
292
|
+
case 'number':
|
|
293
|
+
return definition.precision !== undefined && definition.scale !== undefined ? 'decimal' : 'float'
|
|
294
|
+
case 'boolean':
|
|
295
|
+
return 'boolean'
|
|
296
|
+
case 'date':
|
|
297
|
+
return 'date'
|
|
298
|
+
case 'dateTime':
|
|
299
|
+
return 'datetime'
|
|
300
|
+
case 'time':
|
|
301
|
+
return 'time'
|
|
302
|
+
case 'timestamp':
|
|
303
|
+
return 'integer'
|
|
304
|
+
case 'array':
|
|
305
|
+
case 'object':
|
|
306
|
+
case 'serialize':
|
|
307
|
+
return 'json'
|
|
308
|
+
case 'blob':
|
|
309
|
+
case 'file':
|
|
310
|
+
return 'binary'
|
|
311
|
+
case 'string':
|
|
312
|
+
case 'none':
|
|
313
|
+
default:
|
|
314
|
+
return 'string'
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function normalizeDesiredTypeKind (definition = {}) {
|
|
319
|
+
if (Array.isArray(definition.enum) && definition.enum.length > 0) {
|
|
320
|
+
return 'string'
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(definition.setValues) && definition.setValues.length > 0) {
|
|
323
|
+
return 'string'
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
switch (definition.type) {
|
|
327
|
+
case 'id':
|
|
328
|
+
return 'integer'
|
|
329
|
+
case 'number':
|
|
330
|
+
return 'number'
|
|
331
|
+
case 'boolean':
|
|
332
|
+
return 'boolean'
|
|
333
|
+
case 'date':
|
|
334
|
+
return 'date'
|
|
335
|
+
case 'dateTime':
|
|
336
|
+
return 'datetime'
|
|
337
|
+
case 'time':
|
|
338
|
+
return 'time'
|
|
339
|
+
case 'array':
|
|
340
|
+
case 'object':
|
|
341
|
+
case 'serialize':
|
|
342
|
+
return 'json'
|
|
343
|
+
case 'blob':
|
|
344
|
+
case 'file':
|
|
345
|
+
return 'binary'
|
|
346
|
+
case 'timestamp':
|
|
347
|
+
return 'integer'
|
|
16
348
|
case 'string':
|
|
349
|
+
case 'none':
|
|
350
|
+
default:
|
|
351
|
+
return 'string'
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeComparableDefault (value) {
|
|
356
|
+
if (value === undefined) return undefined
|
|
357
|
+
if (typeof value === 'boolean') return value ? '1' : '0'
|
|
358
|
+
if (value === null) return null
|
|
359
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
360
|
+
return String(value)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeDesiredColumn (tableContext, fieldName, definition, options = {}) {
|
|
364
|
+
const nullable = definition.nullable === true
|
|
365
|
+
? true
|
|
366
|
+
: !(definition.required === true || definition.nullable === false)
|
|
367
|
+
const shape = normalizeColumnShape(definition)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
fieldName,
|
|
371
|
+
name: getColumnNameForDefinition(tableContext, fieldName),
|
|
372
|
+
definition,
|
|
373
|
+
shape,
|
|
374
|
+
typeKind: normalizeDesiredTypeKind(definition),
|
|
375
|
+
nullable,
|
|
376
|
+
required: definition.required === true,
|
|
377
|
+
hasDefault: definition.defaultTo !== undefined,
|
|
378
|
+
defaultValue: normalizeComparableDefault(definition.defaultTo),
|
|
379
|
+
unsigned: definition.type === 'id' ? definition.unsigned !== false : definition.unsigned === true,
|
|
380
|
+
maxLength: definition.maxLength ?? null,
|
|
381
|
+
numericPrecision: definition.precision ?? null,
|
|
382
|
+
numericScale: definition.scale ?? null,
|
|
383
|
+
datetimePrecision: definition.temporalPrecision ?? null,
|
|
384
|
+
enumValues: Array.isArray(definition.enum) ? definition.enum.map((value) => String(value)) : [],
|
|
385
|
+
setValues: Array.isArray(definition.setValues) ? definition.setValues.map((value) => String(value)) : [],
|
|
386
|
+
autoIncrement: options.autoIncrement === true
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildImplicitIdDefinition (idColumn) {
|
|
391
|
+
return {
|
|
392
|
+
type: 'id',
|
|
393
|
+
primary: true,
|
|
394
|
+
storage: {
|
|
395
|
+
column: idColumn
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function resolveTableSchemaContext (schemaLike, options = {}) {
|
|
401
|
+
const schemaStructure = schemaLike?.structure || schemaLike || {}
|
|
402
|
+
const configuredIdColumn = options.idProperty || schemaStructure.id?.storage?.column || 'id'
|
|
403
|
+
const tableContext = createTableBuilderContext(schemaStructure, configuredIdColumn)
|
|
404
|
+
|
|
405
|
+
let resourceIdField = null
|
|
406
|
+
for (const [fieldName, definition] of Object.entries(schemaStructure)) {
|
|
407
|
+
if (definition?.type !== 'id') continue
|
|
408
|
+
const columnName = getColumnNameForDefinition(tableContext, fieldName)
|
|
409
|
+
if (columnName === configuredIdColumn) {
|
|
410
|
+
resourceIdField = fieldName
|
|
411
|
+
break
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const implicitIndexes = normalizeFieldIndexes(schemaStructure, options.tableName || 'table', tableContext)
|
|
416
|
+
const explicitIndexes = normalizeTopLevelIndexes(schemaLike, options.tableName || 'table', tableContext)
|
|
417
|
+
const implicitForeignKeys = normalizeFieldForeignKeys(schemaStructure, options.tableName || 'table', tableContext)
|
|
418
|
+
const explicitForeignKeys = normalizeTopLevelForeignKeys(schemaLike, options.tableName || 'table', tableContext)
|
|
419
|
+
const checkConstraints = normalizeCheckConstraints(schemaLike, options.tableName || 'table')
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
schemaStructure,
|
|
423
|
+
idColumn: configuredIdColumn,
|
|
424
|
+
resourceIdField,
|
|
425
|
+
hasPrimaryIdField: resourceIdField
|
|
426
|
+
? schemaStructure[resourceIdField]?.primary === true
|
|
427
|
+
: false,
|
|
428
|
+
tableContext,
|
|
429
|
+
indexes: dedupeBySignature(
|
|
430
|
+
[...implicitIndexes, ...explicitIndexes],
|
|
431
|
+
(entry) => `${entry.name}|${entry.unique ? 'unique' : 'index'}|${entry.columns.join(',')}`,
|
|
432
|
+
compareByName
|
|
433
|
+
),
|
|
434
|
+
foreignKeys: dedupeBySignature(
|
|
435
|
+
[...implicitForeignKeys, ...explicitForeignKeys],
|
|
436
|
+
(entry) => `${entry.name}|${entry.columns.join(',')}|${entry.referencedTableName}|${entry.referencedColumns.join(',')}`,
|
|
437
|
+
compareByName
|
|
438
|
+
),
|
|
439
|
+
checkConstraints: dedupeBySignature(
|
|
440
|
+
checkConstraints,
|
|
441
|
+
(entry) => `${entry.name}|${entry.clause}`,
|
|
442
|
+
compareByName
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function mapTypeToKnex (table, columnName, definition, options = {}) {
|
|
448
|
+
const {
|
|
449
|
+
precision,
|
|
450
|
+
scale,
|
|
451
|
+
maxLength,
|
|
452
|
+
unsigned,
|
|
453
|
+
temporalPrecision
|
|
454
|
+
} = definition
|
|
455
|
+
const enumValues = Array.isArray(definition.enum) ? definition.enum : []
|
|
456
|
+
const setValues = Array.isArray(definition.setValues) ? definition.setValues : []
|
|
457
|
+
const dialect = normalizeText(options.dialect).toLowerCase()
|
|
458
|
+
|
|
459
|
+
if (setValues.length > 0) {
|
|
460
|
+
if (!isMysqlDialect(dialect)) {
|
|
461
|
+
throw new Error(`Field '${columnName}' uses setValues, which requires a MySQL-compatible knex client.`)
|
|
462
|
+
}
|
|
463
|
+
return table.specificType(columnName, buildSetColumnType(setValues))
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
switch (definition.type) {
|
|
467
|
+
case 'string':
|
|
468
|
+
if (enumValues.length > 0) {
|
|
469
|
+
return table.enu(columnName, enumValues)
|
|
470
|
+
}
|
|
17
471
|
return maxLength ? table.string(columnName, maxLength) : table.string(columnName)
|
|
18
472
|
|
|
19
473
|
case 'number':
|
|
@@ -22,9 +476,10 @@ function mapTypeToKnex (table, columnName, definition) {
|
|
|
22
476
|
}
|
|
23
477
|
return table.float(columnName)
|
|
24
478
|
|
|
25
|
-
case 'id':
|
|
479
|
+
case 'id': {
|
|
26
480
|
const col = table.integer(columnName)
|
|
27
481
|
return unsigned !== false ? col.unsigned() : col
|
|
482
|
+
}
|
|
28
483
|
|
|
29
484
|
case 'boolean':
|
|
30
485
|
return table.boolean(columnName)
|
|
@@ -33,10 +488,14 @@ function mapTypeToKnex (table, columnName, definition) {
|
|
|
33
488
|
return table.date(columnName)
|
|
34
489
|
|
|
35
490
|
case 'dateTime':
|
|
36
|
-
return
|
|
491
|
+
return temporalPrecision != null
|
|
492
|
+
? table.datetime(columnName, { precision: temporalPrecision })
|
|
493
|
+
: table.datetime(columnName)
|
|
37
494
|
|
|
38
495
|
case 'time':
|
|
39
|
-
return
|
|
496
|
+
return temporalPrecision != null
|
|
497
|
+
? table.time(columnName, { precision: temporalPrecision })
|
|
498
|
+
: table.time(columnName)
|
|
40
499
|
|
|
41
500
|
case 'timestamp':
|
|
42
501
|
return table.integer(columnName)
|
|
@@ -56,61 +515,368 @@ function mapTypeToKnex (table, columnName, definition) {
|
|
|
56
515
|
}
|
|
57
516
|
}
|
|
58
517
|
|
|
59
|
-
|
|
60
|
-
* Applies schema constraints to a Knex column
|
|
61
|
-
* @param {object} column - The Knex column builder instance
|
|
62
|
-
* @param {object} definition - The schema definition for the field
|
|
63
|
-
*/
|
|
64
|
-
function applyConstraints (column, definition) {
|
|
65
|
-
// Nullability
|
|
518
|
+
function applyColumnConstraints (column, definition) {
|
|
66
519
|
if (definition.nullable === true) {
|
|
67
520
|
column.nullable()
|
|
68
521
|
} else if (definition.required === true || definition.nullable === false) {
|
|
69
522
|
column.notNullable()
|
|
70
523
|
}
|
|
71
524
|
|
|
72
|
-
// Default value
|
|
73
525
|
if (definition.defaultTo !== undefined) {
|
|
74
526
|
column.defaultTo(definition.defaultTo)
|
|
75
527
|
}
|
|
76
528
|
|
|
77
|
-
// Database-specific constraints
|
|
78
|
-
if (definition.unique === true) {
|
|
79
|
-
column.unique()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
529
|
if (definition.primary === true) {
|
|
83
530
|
column.primary()
|
|
84
531
|
}
|
|
85
532
|
|
|
86
|
-
if (definition.
|
|
87
|
-
column.
|
|
533
|
+
if (definition.comment) {
|
|
534
|
+
column.comment(definition.comment)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function applyTableMetadata (table, tableSchemaContext) {
|
|
539
|
+
for (const index of tableSchemaContext.indexes) {
|
|
540
|
+
if (index.unique) {
|
|
541
|
+
table.unique(index.columns, index.name)
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const indexOptions = index.indexType
|
|
546
|
+
? { indexType: index.indexType }
|
|
547
|
+
: undefined
|
|
548
|
+
table.index(index.columns, index.name, indexOptions)
|
|
88
549
|
}
|
|
89
550
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
551
|
+
for (const foreignKey of tableSchemaContext.foreignKeys) {
|
|
552
|
+
const reference = table.foreign(foreignKey.columns, foreignKey.name)
|
|
553
|
+
.references(foreignKey.referencedColumns)
|
|
554
|
+
.inTable(foreignKey.referencedTableName)
|
|
93
555
|
|
|
94
|
-
if (
|
|
95
|
-
|
|
556
|
+
if (foreignKey.deleteRule) {
|
|
557
|
+
reference.onDelete(foreignKey.deleteRule)
|
|
96
558
|
}
|
|
97
559
|
|
|
98
|
-
if (
|
|
99
|
-
|
|
560
|
+
if (foreignKey.updateRule) {
|
|
561
|
+
reference.onUpdate(foreignKey.updateRule)
|
|
100
562
|
}
|
|
101
563
|
}
|
|
102
564
|
|
|
565
|
+
for (const constraint of tableSchemaContext.checkConstraints) {
|
|
566
|
+
table.check(constraint.clause, [], constraint.name)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function buildColumnBuilderCode (columnName, definition, options = {}) {
|
|
571
|
+
const dialect = normalizeText(options.dialect).toLowerCase()
|
|
572
|
+
const enumValues = Array.isArray(definition.enum) ? definition.enum : []
|
|
573
|
+
const setValues = Array.isArray(definition.setValues) ? definition.setValues : []
|
|
574
|
+
let line = ''
|
|
575
|
+
|
|
576
|
+
if (setValues.length > 0) {
|
|
577
|
+
if (dialect && !isMysqlDialect(dialect)) {
|
|
578
|
+
throw new Error(`Field '${columnName}' uses setValues, which requires a MySQL-compatible migration target.`)
|
|
579
|
+
}
|
|
580
|
+
line = `table.specificType(${quoteJsString(columnName)}, ${quoteJsString(buildSetColumnType(setValues))})`
|
|
581
|
+
} else {
|
|
582
|
+
switch (definition.type) {
|
|
583
|
+
case 'string':
|
|
584
|
+
if (enumValues.length > 0) {
|
|
585
|
+
line = `table.enu(${quoteJsString(columnName)}, ${formatArrayLiteral(enumValues)})`
|
|
586
|
+
} else {
|
|
587
|
+
line = definition.maxLength
|
|
588
|
+
? `table.string(${quoteJsString(columnName)}, ${definition.maxLength})`
|
|
589
|
+
: `table.string(${quoteJsString(columnName)})`
|
|
590
|
+
}
|
|
591
|
+
break
|
|
592
|
+
|
|
593
|
+
case 'number':
|
|
594
|
+
if (definition.precision !== undefined && definition.scale !== undefined) {
|
|
595
|
+
line = `table.decimal(${quoteJsString(columnName)}, ${definition.precision}, ${definition.scale})`
|
|
596
|
+
} else {
|
|
597
|
+
line = `table.float(${quoteJsString(columnName)})`
|
|
598
|
+
}
|
|
599
|
+
break
|
|
600
|
+
|
|
601
|
+
case 'id':
|
|
602
|
+
line = `table.integer(${quoteJsString(columnName)})`
|
|
603
|
+
if (definition.unsigned !== false) {
|
|
604
|
+
line += '.unsigned()'
|
|
605
|
+
}
|
|
606
|
+
break
|
|
607
|
+
|
|
608
|
+
case 'boolean':
|
|
609
|
+
line = `table.boolean(${quoteJsString(columnName)})`
|
|
610
|
+
break
|
|
611
|
+
|
|
612
|
+
case 'date':
|
|
613
|
+
line = `table.date(${quoteJsString(columnName)})`
|
|
614
|
+
break
|
|
615
|
+
|
|
616
|
+
case 'dateTime':
|
|
617
|
+
if (definition.temporalPrecision != null) {
|
|
618
|
+
line = `table.datetime(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision} })`
|
|
619
|
+
} else {
|
|
620
|
+
line = `table.datetime(${quoteJsString(columnName)})`
|
|
621
|
+
}
|
|
622
|
+
break
|
|
623
|
+
|
|
624
|
+
case 'time':
|
|
625
|
+
if (definition.temporalPrecision != null) {
|
|
626
|
+
line = `table.time(${quoteJsString(columnName)}, { precision: ${definition.temporalPrecision} })`
|
|
627
|
+
} else {
|
|
628
|
+
line = `table.time(${quoteJsString(columnName)})`
|
|
629
|
+
}
|
|
630
|
+
break
|
|
631
|
+
|
|
632
|
+
case 'timestamp':
|
|
633
|
+
line = `table.integer(${quoteJsString(columnName)})`
|
|
634
|
+
break
|
|
635
|
+
|
|
636
|
+
case 'array':
|
|
637
|
+
case 'object':
|
|
638
|
+
case 'serialize':
|
|
639
|
+
line = `table.json(${quoteJsString(columnName)})`
|
|
640
|
+
break
|
|
641
|
+
|
|
642
|
+
case 'blob':
|
|
643
|
+
case 'file':
|
|
644
|
+
line = `table.binary(${quoteJsString(columnName)})`
|
|
645
|
+
break
|
|
646
|
+
|
|
647
|
+
case 'none':
|
|
648
|
+
default:
|
|
649
|
+
line = `table.string(${quoteJsString(columnName)})`
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (definition.nullable === true) {
|
|
654
|
+
line += '.nullable()'
|
|
655
|
+
} else if (definition.required === true || definition.nullable === false) {
|
|
656
|
+
line += '.notNullable()'
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (definition.defaultTo !== undefined) {
|
|
660
|
+
line += `.defaultTo(${formatCodeLiteral(definition.defaultTo)})`
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (definition.primary === true && options.includePrimary !== false) {
|
|
664
|
+
line += '.primary()'
|
|
665
|
+
}
|
|
666
|
+
|
|
103
667
|
if (definition.comment) {
|
|
104
|
-
|
|
668
|
+
line += `.comment(${quoteJsString(definition.comment)})`
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (options.alter === true) {
|
|
672
|
+
line += '.alter()'
|
|
105
673
|
}
|
|
674
|
+
|
|
675
|
+
return line
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function buildIndexLine (index) {
|
|
679
|
+
if (index.unique) {
|
|
680
|
+
return `table.unique(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)})`
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (index.indexType) {
|
|
684
|
+
return `table.index(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)}, ${formatCodeLiteral({ indexType: index.indexType })})`
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return `table.index(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)})`
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function buildForeignKeyLine (foreignKey) {
|
|
691
|
+
let line = `table.foreign(${formatCodeLiteral(foreignKey.columns)}, ${quoteJsString(foreignKey.name)})`
|
|
692
|
+
line += `.references(${formatCodeLiteral(foreignKey.referencedColumns)})`
|
|
693
|
+
line += `.inTable(${quoteJsString(foreignKey.referencedTableName)})`
|
|
694
|
+
|
|
695
|
+
if (foreignKey.deleteRule) {
|
|
696
|
+
line += `.onDelete(${quoteJsString(foreignKey.deleteRule)})`
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (foreignKey.updateRule) {
|
|
700
|
+
line += `.onUpdate(${quoteJsString(foreignKey.updateRule)})`
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return line
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function buildCheckConstraintLine (constraint) {
|
|
707
|
+
return `table.check(${quoteJsString(constraint.clause)}, [], ${quoteJsString(constraint.name)})`
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function buildDesiredColumnsMap (tableSchemaContext, options = {}) {
|
|
711
|
+
const desiredColumns = new Map()
|
|
712
|
+
|
|
713
|
+
if (!tableSchemaContext.hasPrimaryIdField && options.autoIncrement === true) {
|
|
714
|
+
const implicitIdDefinition = buildImplicitIdDefinition(tableSchemaContext.idColumn)
|
|
715
|
+
desiredColumns.set(
|
|
716
|
+
tableSchemaContext.idColumn,
|
|
717
|
+
normalizeDesiredColumn(
|
|
718
|
+
createTableBuilderContext({ id: implicitIdDefinition }, tableSchemaContext.idColumn),
|
|
719
|
+
'id',
|
|
720
|
+
implicitIdDefinition,
|
|
721
|
+
{ autoIncrement: true }
|
|
722
|
+
)
|
|
723
|
+
)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
727
|
+
if (fieldName === tableSchemaContext.resourceIdField && !tableSchemaContext.hasPrimaryIdField && options.autoIncrement === true) {
|
|
728
|
+
continue
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
desiredColumns.set(
|
|
732
|
+
getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName),
|
|
733
|
+
normalizeDesiredColumn(tableSchemaContext.tableContext, fieldName, definition)
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return desiredColumns
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function valuesEqual (left, right) {
|
|
741
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeSnapshotDefaultValue (column = {}) {
|
|
745
|
+
if (!column.hasDefault) return undefined
|
|
746
|
+
return normalizeComparableDefault(column.defaultValue)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function collectDestructiveColumnWarnings (currentColumn, desiredColumn) {
|
|
750
|
+
const warnings = []
|
|
751
|
+
const columnName = desiredColumn.name
|
|
752
|
+
|
|
753
|
+
if (currentColumn.nullable === true && desiredColumn.nullable === false) {
|
|
754
|
+
warnings.push(`Column '${columnName}' changes from nullable to not-null and may fail on existing rows.`)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (
|
|
758
|
+
currentColumn.maxLength != null &&
|
|
759
|
+
desiredColumn.maxLength != null &&
|
|
760
|
+
desiredColumn.maxLength < currentColumn.maxLength
|
|
761
|
+
) {
|
|
762
|
+
warnings.push(`Column '${columnName}' reduces maxLength from ${currentColumn.maxLength} to ${desiredColumn.maxLength}.`)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (
|
|
766
|
+
currentColumn.numericPrecision != null &&
|
|
767
|
+
desiredColumn.numericPrecision != null &&
|
|
768
|
+
desiredColumn.numericPrecision < currentColumn.numericPrecision
|
|
769
|
+
) {
|
|
770
|
+
warnings.push(`Column '${columnName}' reduces numeric precision from ${currentColumn.numericPrecision} to ${desiredColumn.numericPrecision}.`)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (
|
|
774
|
+
currentColumn.numericScale != null &&
|
|
775
|
+
desiredColumn.numericScale != null &&
|
|
776
|
+
desiredColumn.numericScale < currentColumn.numericScale
|
|
777
|
+
) {
|
|
778
|
+
warnings.push(`Column '${columnName}' reduces numeric scale from ${currentColumn.numericScale} to ${desiredColumn.numericScale}.`)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (!valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues || [])) {
|
|
782
|
+
warnings.push(`Column '${columnName}' changes enum values and may invalidate existing data.`)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (!valuesEqual(currentColumn.setValues || [], desiredColumn.setValues || [])) {
|
|
786
|
+
warnings.push(`Column '${columnName}' changes setValues and may invalidate existing data.`)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (normalizeText(currentColumn.typeKind) !== normalizeText(desiredColumn.typeKind)) {
|
|
790
|
+
warnings.push(`Column '${columnName}' changes type semantics from '${currentColumn.typeKind}' to '${desiredColumn.typeKind}'.`)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return warnings
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function columnNeedsAlter (currentColumn, desiredColumn, options = {}) {
|
|
797
|
+
const currentNumericShape = (() => {
|
|
798
|
+
const dataType = normalizeText(currentColumn.dataType)
|
|
799
|
+
if (dataType === 'decimal' || dataType === 'numeric') return 'decimal'
|
|
800
|
+
if (dataType === 'float' || dataType === 'double' || dataType === 'real') return 'float'
|
|
801
|
+
return ''
|
|
802
|
+
})()
|
|
803
|
+
const compareUnsigned = isMysqlDialect(normalizeText(options.dialect).toLowerCase())
|
|
804
|
+
|
|
805
|
+
return (
|
|
806
|
+
normalizeText(currentColumn.typeKind) !== normalizeText(desiredColumn.typeKind) ||
|
|
807
|
+
(desiredColumn.typeKind === 'number' && currentNumericShape && currentNumericShape !== normalizeText(desiredColumn.shape)) ||
|
|
808
|
+
Boolean(currentColumn.nullable) !== Boolean(desiredColumn.nullable) ||
|
|
809
|
+
(compareUnsigned && Boolean(currentColumn.unsigned) !== Boolean(desiredColumn.unsigned)) ||
|
|
810
|
+
(currentColumn.maxLength ?? null) !== (desiredColumn.maxLength ?? null) ||
|
|
811
|
+
(currentColumn.numericPrecision ?? null) !== (desiredColumn.numericPrecision ?? null) ||
|
|
812
|
+
(currentColumn.numericScale ?? null) !== (desiredColumn.numericScale ?? null) ||
|
|
813
|
+
normalizeSnapshotDefaultValue(currentColumn) !== desiredColumn.defaultValue ||
|
|
814
|
+
Boolean(currentColumn.hasDefault) !== Boolean(desiredColumn.hasDefault) ||
|
|
815
|
+
!valuesEqual(currentColumn.enumValues || [], desiredColumn.enumValues || []) ||
|
|
816
|
+
!valuesEqual(currentColumn.setValues || [], desiredColumn.setValues || [])
|
|
817
|
+
)
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function mapByName (entries = []) {
|
|
821
|
+
return new Map((entries || []).map((entry) => [entry.name, entry]))
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function createEmptyDiffPlan () {
|
|
825
|
+
return {
|
|
826
|
+
addColumns: [],
|
|
827
|
+
alterColumns: [],
|
|
828
|
+
dropColumns: [],
|
|
829
|
+
addIndexes: [],
|
|
830
|
+
dropIndexes: [],
|
|
831
|
+
addForeignKeys: [],
|
|
832
|
+
dropForeignKeys: [],
|
|
833
|
+
addCheckConstraints: [],
|
|
834
|
+
warnings: []
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function assertNoTopLevelTableMetadata (schemaLike, helperName) {
|
|
839
|
+
const metadataKeys = ['indexes', 'foreignKeys', 'checkConstraints']
|
|
840
|
+
|
|
841
|
+
for (const key of metadataKeys) {
|
|
842
|
+
if (Array.isArray(schemaLike?.[key]) && schemaLike[key].length > 0) {
|
|
843
|
+
throw new Error(`${helperName} does not accept top-level ${key}. Use createKnexTable(), generateKnexMigration(), or generateKnexMigrationDiff() for table metadata.`)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function buildUnsupportedConstraintWarning (constraintType, name, dialect) {
|
|
849
|
+
return `Skipping ${constraintType} '${name}' because automatic ALTER support is not available for dialect '${dialect || 'unknown'}'.`
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function isSqliteDialectName (dialect = '') {
|
|
853
|
+
return String(dialect || '').toLowerCase().includes('sqlite')
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function normalizeConstraintClause (clause = '') {
|
|
857
|
+
return normalizeText(clause)
|
|
858
|
+
.replace(/[`"]/g, '')
|
|
859
|
+
.replace(/\s*\(\s*/g, '(')
|
|
860
|
+
.replace(/\s*\)\s*/g, ')')
|
|
861
|
+
.replace(/\s*,\s*/g, ',')
|
|
862
|
+
.replace(/\s+/g, ' ')
|
|
863
|
+
.toLowerCase()
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function isInlineEnumCheckConstraint (currentCheck = {}, desiredColumn = {}) {
|
|
867
|
+
if (!desiredColumn.enumValues || desiredColumn.enumValues.length < 1) {
|
|
868
|
+
return false
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const expectedClause = `${desiredColumn.name} in (${desiredColumn.enumValues.map((value) => quoteJsString(value)).join(', ')})`
|
|
872
|
+
return normalizeConstraintClause(currentCheck.clause) === normalizeConstraintClause(expectedClause)
|
|
106
873
|
}
|
|
107
874
|
|
|
108
875
|
/**
|
|
109
876
|
* Creates a Knex table from a json-rest-schema definition
|
|
110
877
|
* @param {object} knex - The Knex instance
|
|
111
|
-
* @param {
|
|
112
|
-
* @param {object}
|
|
113
|
-
* @param {string} [idProperty='id'] - The name of the ID column
|
|
878
|
+
* @param {object} schemaInfo - Resource schema metadata
|
|
879
|
+
* @param {object} tableSchemaInstance - The json-rest-schema instance or schema-like table metadata object
|
|
114
880
|
* @param {object} [options={}] - Additional options
|
|
115
881
|
* @param {boolean} [options.autoIncrement=true] - Whether to use auto-incrementing IDs
|
|
116
882
|
* @param {boolean} [options.timestamps=false] - Whether to add created_at/updated_at columns
|
|
@@ -118,62 +884,68 @@ function applyConstraints (column, definition) {
|
|
|
118
884
|
*/
|
|
119
885
|
export async function createKnexTable (knex, schemaInfo, tableSchemaInstance, options = {}) {
|
|
120
886
|
const { autoIncrement = true, timestamps = false } = options
|
|
121
|
-
|
|
122
887
|
const tableName = schemaInfo.tableName
|
|
123
|
-
const
|
|
124
|
-
const
|
|
888
|
+
const dialect = detectKnexDialect(knex)
|
|
889
|
+
const tableSchemaContext = resolveTableSchemaContext(tableSchemaInstance, {
|
|
890
|
+
idProperty: schemaInfo.idProperty,
|
|
891
|
+
tableName
|
|
892
|
+
})
|
|
125
893
|
|
|
126
894
|
return knex.schema.createTable(tableName, (table) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Add auto-incrementing ID if no primary key is defined and autoIncrement is true
|
|
131
|
-
if (!hasIdField && autoIncrement) {
|
|
132
|
-
table.increments(idProperty).primary()
|
|
895
|
+
if (!tableSchemaContext.hasPrimaryIdField && autoIncrement) {
|
|
896
|
+
table.increments(tableSchemaContext.idColumn).primary()
|
|
133
897
|
}
|
|
134
898
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Skip if this is the ID field and we already handled it
|
|
138
|
-
if (fieldName === idProperty && !hasIdField && autoIncrement) {
|
|
899
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
900
|
+
if (fieldName === tableSchemaContext.resourceIdField && !tableSchemaContext.hasPrimaryIdField && autoIncrement) {
|
|
139
901
|
continue
|
|
140
902
|
}
|
|
141
903
|
|
|
142
|
-
|
|
143
|
-
const column = mapTypeToKnex(table,
|
|
144
|
-
|
|
145
|
-
// Apply constraints
|
|
146
|
-
applyConstraints(column, definition)
|
|
904
|
+
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
905
|
+
const column = mapTypeToKnex(table, columnName, definition, { dialect })
|
|
906
|
+
applyColumnConstraints(column, definition)
|
|
147
907
|
}
|
|
148
908
|
|
|
149
|
-
|
|
909
|
+
applyTableMetadata(table, tableSchemaContext)
|
|
910
|
+
|
|
150
911
|
if (timestamps) {
|
|
151
912
|
table.timestamps(true, true)
|
|
152
913
|
}
|
|
153
914
|
})
|
|
154
915
|
}
|
|
155
916
|
|
|
156
|
-
export async function addKnexFields (knex, tableName, schema) {
|
|
917
|
+
export async function addKnexFields (knex, tableName, schema, options = {}) {
|
|
918
|
+
assertNoTopLevelTableMetadata(schema, 'addKnexFields')
|
|
919
|
+
const dialect = detectKnexDialect(knex)
|
|
920
|
+
const tableSchemaContext = resolveTableSchemaContext(schema, {
|
|
921
|
+
...options,
|
|
922
|
+
tableName
|
|
923
|
+
})
|
|
924
|
+
|
|
157
925
|
return knex.schema.alterTable(tableName, (table) => {
|
|
158
|
-
for (const [fieldName, definition] of Object.entries(
|
|
159
|
-
const
|
|
160
|
-
|
|
926
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
927
|
+
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
928
|
+
const column = mapTypeToKnex(table, columnName, definition, { dialect })
|
|
929
|
+
applyColumnConstraints(column, definition)
|
|
161
930
|
}
|
|
162
931
|
})
|
|
163
932
|
}
|
|
164
933
|
|
|
165
934
|
// Helper function to alter multiple fields in an existing table
|
|
166
935
|
export async function alterKnexFields (knex, tableName, fields, options = {}) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
936
|
+
assertNoTopLevelTableMetadata(fields, 'alterKnexFields')
|
|
937
|
+
const dialect = detectKnexDialect(knex)
|
|
938
|
+
const tableSchemaContext = resolveTableSchemaContext(fields, {
|
|
939
|
+
...options,
|
|
940
|
+
tableName
|
|
941
|
+
})
|
|
171
942
|
|
|
172
|
-
|
|
943
|
+
return knex.schema.alterTable(tableName, (table) => {
|
|
944
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
945
|
+
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
946
|
+
const column = mapTypeToKnex(table, columnName, definition, { dialect })
|
|
173
947
|
column.alter()
|
|
174
|
-
|
|
175
|
-
// Apply constraints (these will be reapplied with the alter)
|
|
176
|
-
applyConstraints(column, definition)
|
|
948
|
+
applyColumnConstraints(column, definition)
|
|
177
949
|
}
|
|
178
950
|
})
|
|
179
951
|
}
|
|
@@ -181,124 +953,296 @@ export async function alterKnexFields (knex, tableName, fields, options = {}) {
|
|
|
181
953
|
/**
|
|
182
954
|
* Generates a Knex migration string from a json-rest-schema definition
|
|
183
955
|
* @param {string} tableName - The name of the table
|
|
184
|
-
* @param {object} schema - The json-rest-schema instance
|
|
956
|
+
* @param {object} schema - The json-rest-schema instance or schema-like table metadata object
|
|
185
957
|
* @param {object} [options={}] - Additional options
|
|
186
958
|
* @returns {string} The migration code as a string
|
|
187
959
|
*/
|
|
188
960
|
export function generateKnexMigration (tableName, schema, options = {}) {
|
|
189
|
-
const { autoIncrement = true, timestamps = false } = options
|
|
961
|
+
const { autoIncrement = true, timestamps = false, dialect = '' } = options
|
|
962
|
+
const tableSchemaContext = resolveTableSchemaContext(schema, {
|
|
963
|
+
...options,
|
|
964
|
+
tableName
|
|
965
|
+
})
|
|
190
966
|
|
|
191
|
-
|
|
192
|
-
return knex.schema.createTable('${tableName}', (table) => {\n`
|
|
967
|
+
const lines = []
|
|
193
968
|
|
|
194
|
-
|
|
195
|
-
|
|
969
|
+
if (!tableSchemaContext.hasPrimaryIdField && autoIncrement) {
|
|
970
|
+
lines.push(`table.increments(${quoteJsString(tableSchemaContext.idColumn)}).primary()`)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
for (const [fieldName, definition] of Object.entries(tableSchemaContext.schemaStructure)) {
|
|
974
|
+
if (fieldName === tableSchemaContext.resourceIdField && !tableSchemaContext.hasPrimaryIdField && autoIncrement) {
|
|
975
|
+
continue
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const columnName = getColumnNameForDefinition(tableSchemaContext.tableContext, fieldName)
|
|
979
|
+
lines.push(buildColumnBuilderCode(columnName, definition, {
|
|
980
|
+
dialect
|
|
981
|
+
}))
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
for (const index of tableSchemaContext.indexes) {
|
|
985
|
+
lines.push(buildIndexLine(index))
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
for (const foreignKey of tableSchemaContext.foreignKeys) {
|
|
989
|
+
lines.push(buildForeignKeyLine(foreignKey))
|
|
990
|
+
}
|
|
196
991
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
migration += ' table.increments(\'id\').primary();\n'
|
|
992
|
+
for (const checkConstraint of tableSchemaContext.checkConstraints) {
|
|
993
|
+
lines.push(buildCheckConstraintLine(checkConstraint))
|
|
200
994
|
}
|
|
201
995
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
996
|
+
if (timestamps) {
|
|
997
|
+
lines.push('table.timestamps(true, true)')
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return `exports.up = function(knex) {
|
|
1001
|
+
return knex.schema.createTable(${quoteJsString(tableName)}, (table) => {
|
|
1002
|
+
${lines.map((line) => ` ${line};`).join('\n')}
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
exports.down = function(knex) {
|
|
1007
|
+
return knex.schema.dropTable(${quoteJsString(tableName)});
|
|
1008
|
+
};`
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Generates a deterministic Knex alter migration by diffing a live table snapshot against a desired schema.
|
|
1013
|
+
* @param {string} tableName - The table name being diffed
|
|
1014
|
+
* @param {object} currentSnapshot - The normalized table snapshot returned by introspection
|
|
1015
|
+
* @param {object} schema - The desired schema-like table metadata object
|
|
1016
|
+
* @param {object} [options={}] - Diff options
|
|
1017
|
+
* @param {boolean} [options.autoIncrement=true] - Whether the resource expects an implicit auto-increment id column
|
|
1018
|
+
* @param {boolean} [options.allowDropColumns=false] - Whether removed columns should be dropped automatically
|
|
1019
|
+
* @param {string} [options.dialect=''] - Optional target dialect override
|
|
1020
|
+
* @returns {{ migration: string, warnings: string[], plan: object }} Diff result
|
|
1021
|
+
*/
|
|
1022
|
+
export function generateKnexMigrationDiff (tableName, currentSnapshot, schema, options = {}) {
|
|
1023
|
+
const {
|
|
1024
|
+
autoIncrement = true,
|
|
1025
|
+
allowDropColumns = false,
|
|
1026
|
+
dialect = ''
|
|
1027
|
+
} = options
|
|
1028
|
+
const resolvedDialect = normalizeText(dialect || currentSnapshot?.dialect).toLowerCase()
|
|
1029
|
+
const tableSchemaContext = resolveTableSchemaContext(schema, {
|
|
1030
|
+
...options,
|
|
1031
|
+
tableName
|
|
1032
|
+
})
|
|
1033
|
+
const desiredColumns = buildDesiredColumnsMap(tableSchemaContext, { autoIncrement })
|
|
1034
|
+
const currentColumns = mapByName(currentSnapshot?.columns || [])
|
|
1035
|
+
const desiredIndexes = mapByName(tableSchemaContext.indexes)
|
|
1036
|
+
const currentIndexes = mapByName(currentSnapshot?.indexes || [])
|
|
1037
|
+
const desiredForeignKeys = mapByName(tableSchemaContext.foreignKeys)
|
|
1038
|
+
const currentForeignKeys = mapByName(currentSnapshot?.foreignKeys || [])
|
|
1039
|
+
const desiredChecks = mapByName(tableSchemaContext.checkConstraints)
|
|
1040
|
+
const currentChecks = mapByName(currentSnapshot?.checkConstraints || [])
|
|
1041
|
+
const plan = createEmptyDiffPlan()
|
|
1042
|
+
|
|
1043
|
+
for (const [columnName, desiredColumn] of desiredColumns.entries()) {
|
|
1044
|
+
const currentColumn = currentColumns.get(columnName)
|
|
1045
|
+
|
|
1046
|
+
if (!currentColumn) {
|
|
1047
|
+
if (desiredColumn.autoIncrement) {
|
|
1048
|
+
plan.warnings.push(`Skipping implicit auto-increment id column '${columnName}' because it cannot be added safely to an existing table.`)
|
|
1049
|
+
continue
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (desiredColumn.setValues.length > 0 && !isMysqlDialect(resolvedDialect)) {
|
|
1053
|
+
plan.warnings.push(`Skipping setValues column '${columnName}' because it requires a MySQL-compatible ALTER target.`)
|
|
1054
|
+
continue
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
plan.addColumns.push(desiredColumn)
|
|
205
1058
|
continue
|
|
206
1059
|
}
|
|
207
1060
|
|
|
208
|
-
|
|
1061
|
+
if (desiredColumn.autoIncrement) {
|
|
1062
|
+
continue
|
|
1063
|
+
}
|
|
209
1064
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
case 'number':
|
|
218
|
-
if (definition.precision !== undefined && definition.scale !== undefined) {
|
|
219
|
-
line += `table.decimal('${fieldName}', ${definition.precision}, ${definition.scale})`
|
|
220
|
-
} else {
|
|
221
|
-
line += `table.float('${fieldName}')`
|
|
222
|
-
}
|
|
223
|
-
break
|
|
224
|
-
case 'id':
|
|
225
|
-
line += `table.integer('${fieldName}')`
|
|
226
|
-
if (definition.unsigned !== false) line += '.unsigned()'
|
|
227
|
-
break
|
|
228
|
-
case 'boolean':
|
|
229
|
-
line += `table.boolean('${fieldName}')`
|
|
230
|
-
break
|
|
231
|
-
case 'date':
|
|
232
|
-
line += `table.date('${fieldName}')`
|
|
233
|
-
break
|
|
234
|
-
case 'dateTime':
|
|
235
|
-
line += `table.datetime('${fieldName}')`
|
|
236
|
-
break
|
|
237
|
-
case 'time':
|
|
238
|
-
line += `table.time('${fieldName}')`
|
|
239
|
-
break
|
|
240
|
-
case 'timestamp':
|
|
241
|
-
line += `table.integer('${fieldName}')`
|
|
242
|
-
break
|
|
243
|
-
case 'array':
|
|
244
|
-
case 'object':
|
|
245
|
-
case 'serialize':
|
|
246
|
-
line += `table.json('${fieldName}')`
|
|
247
|
-
break
|
|
248
|
-
case 'blob':
|
|
249
|
-
case 'file':
|
|
250
|
-
line += `table.binary('${fieldName}')`
|
|
251
|
-
break
|
|
252
|
-
default:
|
|
253
|
-
line += `table.string('${fieldName}')`
|
|
1065
|
+
if (!columnNeedsAlter(currentColumn, desiredColumn, { dialect: resolvedDialect })) {
|
|
1066
|
+
continue
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (desiredColumn.setValues.length > 0 && !isMysqlDialect(resolvedDialect)) {
|
|
1070
|
+
plan.warnings.push(`Skipping setValues alteration for column '${columnName}' because it requires a MySQL-compatible ALTER target.`)
|
|
1071
|
+
continue
|
|
254
1072
|
}
|
|
255
1073
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1074
|
+
plan.alterColumns.push(desiredColumn)
|
|
1075
|
+
plan.warnings.push(...collectDestructiveColumnWarnings(currentColumn, desiredColumn))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
for (const currentColumn of currentColumns.values()) {
|
|
1079
|
+
if (desiredColumns.has(currentColumn.name)) {
|
|
1080
|
+
continue
|
|
261
1081
|
}
|
|
262
1082
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
1083
|
+
const warning = `Column '${currentColumn.name}' exists in the live table but not in the desired schema.`
|
|
1084
|
+
if (allowDropColumns) {
|
|
1085
|
+
plan.dropColumns.push(currentColumn)
|
|
1086
|
+
plan.warnings.push(`${warning} It will be dropped because allowDropColumns=true.`)
|
|
1087
|
+
} else {
|
|
1088
|
+
plan.warnings.push(`${warning} Skipping automatic drop.`)
|
|
268
1089
|
}
|
|
1090
|
+
}
|
|
269
1091
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1092
|
+
for (const desiredIndex of desiredIndexes.values()) {
|
|
1093
|
+
const currentIndex = currentIndexes.get(desiredIndex.name)
|
|
1094
|
+
const sameIndexType = normalizeText(currentIndex?.indexType).toUpperCase() === desiredIndex.indexType
|
|
1095
|
+
const indexChanged = Boolean(currentIndex) && (
|
|
1096
|
+
!valuesEqual(currentIndex.columns, desiredIndex.columns) ||
|
|
1097
|
+
Boolean(currentIndex.unique) !== Boolean(desiredIndex.unique) ||
|
|
1098
|
+
!sameIndexType
|
|
1099
|
+
)
|
|
273
1100
|
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
line += `.onDelete('${definition.references.onDelete}')`
|
|
279
|
-
}
|
|
280
|
-
if (definition.references.onUpdate) {
|
|
281
|
-
line += `.onUpdate('${definition.references.onUpdate}')`
|
|
1101
|
+
if (!currentIndex || indexChanged) {
|
|
1102
|
+
plan.addIndexes.push(desiredIndex)
|
|
1103
|
+
if (indexChanged) {
|
|
1104
|
+
plan.dropIndexes.push(currentIndex)
|
|
282
1105
|
}
|
|
283
1106
|
}
|
|
1107
|
+
}
|
|
284
1108
|
|
|
285
|
-
|
|
286
|
-
|
|
1109
|
+
for (const currentIndex of currentIndexes.values()) {
|
|
1110
|
+
if (!desiredIndexes.has(currentIndex.name)) {
|
|
1111
|
+
plan.dropIndexes.push(currentIndex)
|
|
287
1112
|
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
for (const desiredForeignKey of desiredForeignKeys.values()) {
|
|
1116
|
+
const currentForeignKey = currentForeignKeys.get(desiredForeignKey.name)
|
|
1117
|
+
const sameColumns = valuesEqual(
|
|
1118
|
+
(currentForeignKey?.columns || []).map((entry) => entry.name),
|
|
1119
|
+
desiredForeignKey.columns
|
|
1120
|
+
)
|
|
1121
|
+
const sameReferencedColumns = valuesEqual(
|
|
1122
|
+
(currentForeignKey?.columns || []).map((entry) => entry.referencedName),
|
|
1123
|
+
desiredForeignKey.referencedColumns
|
|
1124
|
+
)
|
|
1125
|
+
const sameTable = normalizeText(currentForeignKey?.referencedTableName) === desiredForeignKey.referencedTableName
|
|
1126
|
+
const sameDeleteRule = normalizeText(currentForeignKey?.deleteRule).toUpperCase() === desiredForeignKey.deleteRule
|
|
1127
|
+
const sameUpdateRule = normalizeText(currentForeignKey?.updateRule).toUpperCase() === desiredForeignKey.updateRule
|
|
288
1128
|
|
|
289
|
-
|
|
1129
|
+
if (!currentForeignKey || !sameColumns || !sameReferencedColumns || !sameTable || !sameDeleteRule || !sameUpdateRule) {
|
|
1130
|
+
plan.addForeignKeys.push(desiredForeignKey)
|
|
1131
|
+
}
|
|
290
1132
|
}
|
|
291
1133
|
|
|
292
|
-
|
|
293
|
-
|
|
1134
|
+
for (const currentForeignKey of currentForeignKeys.values()) {
|
|
1135
|
+
if (!desiredForeignKeys.has(currentForeignKey.name)) {
|
|
1136
|
+
plan.dropForeignKeys.push(currentForeignKey)
|
|
1137
|
+
}
|
|
294
1138
|
}
|
|
295
1139
|
|
|
296
|
-
|
|
297
|
-
|
|
1140
|
+
for (const desiredCheck of desiredChecks.values()) {
|
|
1141
|
+
const currentCheck = currentChecks.get(desiredCheck.name)
|
|
1142
|
+
if (currentCheck?.clause === desiredCheck.clause) {
|
|
1143
|
+
continue
|
|
1144
|
+
}
|
|
298
1145
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1146
|
+
if (isSqliteDialectName(resolvedDialect)) {
|
|
1147
|
+
plan.warnings.push(buildUnsupportedConstraintWarning('check constraint', desiredCheck.name, resolvedDialect))
|
|
1148
|
+
continue
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
plan.addCheckConstraints.push(desiredCheck)
|
|
1152
|
+
}
|
|
302
1153
|
|
|
303
|
-
|
|
1154
|
+
for (const currentCheck of currentChecks.values()) {
|
|
1155
|
+
const matchesDesiredEnum = [...desiredColumns.values()].some((desiredColumn) => {
|
|
1156
|
+
return isInlineEnumCheckConstraint(currentCheck, desiredColumn)
|
|
1157
|
+
})
|
|
1158
|
+
if (matchesDesiredEnum) {
|
|
1159
|
+
continue
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (!desiredChecks.has(currentCheck.name)) {
|
|
1163
|
+
plan.warnings.push(buildUnsupportedConstraintWarning('check constraint removal', currentCheck.name, resolvedDialect))
|
|
1164
|
+
continue
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
if (desiredChecks.get(currentCheck.name)?.clause !== currentCheck.clause) {
|
|
1168
|
+
plan.warnings.push(buildUnsupportedConstraintWarning('check constraint alteration', currentCheck.name, resolvedDialect))
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
plan.addColumns.sort(compareByName)
|
|
1173
|
+
plan.alterColumns.sort(compareByName)
|
|
1174
|
+
plan.dropColumns.sort(compareByName)
|
|
1175
|
+
plan.addIndexes.sort(compareByName)
|
|
1176
|
+
plan.dropIndexes.sort(compareByName)
|
|
1177
|
+
plan.addForeignKeys.sort(compareByName)
|
|
1178
|
+
plan.dropForeignKeys.sort(compareByName)
|
|
1179
|
+
plan.addCheckConstraints.sort(compareByName)
|
|
1180
|
+
plan.warnings = [...new Set(plan.warnings)]
|
|
1181
|
+
|
|
1182
|
+
const blocks = []
|
|
1183
|
+
|
|
1184
|
+
const addOrAlterColumnLines = [
|
|
1185
|
+
...plan.addColumns.map((column) => buildColumnBuilderCode(column.name, column.definition, { dialect: resolvedDialect })),
|
|
1186
|
+
...plan.alterColumns.map((column) => buildColumnBuilderCode(column.name, column.definition, {
|
|
1187
|
+
dialect: resolvedDialect,
|
|
1188
|
+
alter: true,
|
|
1189
|
+
includePrimary: false
|
|
1190
|
+
}))
|
|
1191
|
+
]
|
|
1192
|
+
|
|
1193
|
+
if (plan.dropForeignKeys.length > 0) {
|
|
1194
|
+
blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
|
|
1195
|
+
${plan.dropForeignKeys.map((foreignKey) => ` table.dropForeign(${formatCodeLiteral(foreignKey.columns.map((entry) => entry.name))}, ${quoteJsString(foreignKey.name)});`).join('\n')}
|
|
1196
|
+
});`)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (plan.dropIndexes.length > 0) {
|
|
1200
|
+
blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
|
|
1201
|
+
${plan.dropIndexes.map((index) => {
|
|
1202
|
+
if (index.unique) {
|
|
1203
|
+
return ` table.dropUnique(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)});`
|
|
1204
|
+
}
|
|
1205
|
+
return ` table.dropIndex(${formatCodeLiteral(index.columns)}, ${quoteJsString(index.name)});`
|
|
1206
|
+
}).join('\n')}
|
|
1207
|
+
});`)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (plan.dropColumns.length > 0) {
|
|
1211
|
+
blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
|
|
1212
|
+
${plan.dropColumns.map((column) => ` table.dropColumn(${quoteJsString(column.name)});`).join('\n')}
|
|
1213
|
+
});`)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (addOrAlterColumnLines.length > 0) {
|
|
1217
|
+
blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
|
|
1218
|
+
${addOrAlterColumnLines.map((line) => ` ${line};`).join('\n')}
|
|
1219
|
+
});`)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const addConstraintLines = [
|
|
1223
|
+
...plan.addIndexes.map((index) => buildIndexLine(index)),
|
|
1224
|
+
...plan.addForeignKeys.map((foreignKey) => buildForeignKeyLine(foreignKey)),
|
|
1225
|
+
...plan.addCheckConstraints.map((constraint) => buildCheckConstraintLine(constraint))
|
|
1226
|
+
]
|
|
1227
|
+
|
|
1228
|
+
if (addConstraintLines.length > 0) {
|
|
1229
|
+
blocks.push(` await knex.schema.alterTable(${quoteJsString(tableName)}, (table) => {
|
|
1230
|
+
${addConstraintLines.map((line) => ` ${line};`).join('\n')}
|
|
1231
|
+
});`)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const warningComment = plan.warnings.length > 0
|
|
1235
|
+
? ` // Warnings:\n${plan.warnings.map((warning) => ` // - ${warning}`).join('\n')}\n`
|
|
1236
|
+
: ''
|
|
1237
|
+
|
|
1238
|
+
return {
|
|
1239
|
+
migration: `exports.up = async function(knex) {
|
|
1240
|
+
${warningComment}${blocks.length > 0 ? `${blocks.join('\n\n')}\n` : ' return Promise.resolve();\n'}};
|
|
1241
|
+
|
|
1242
|
+
exports.down = async function() {
|
|
1243
|
+
throw new Error(${quoteJsString(`Down migration was not generated automatically for table '${tableName}'.`)});
|
|
1244
|
+
};`,
|
|
1245
|
+
warnings: plan.warnings,
|
|
1246
|
+
plan
|
|
1247
|
+
}
|
|
304
1248
|
}
|