@platformatic/sql-mapper 0.10.0 → 0.12.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/lib/entity.js +79 -21
- package/lib/queries/mysql-shared.js +2 -2
- package/lib/queries/pg.js +2 -2
- package/lib/queries/sqlite.js +21 -1
- package/mapper.js +10 -1
- package/package.json +7 -7
- package/test/entity.test.js +152 -0
- package/test/helper.js +11 -1
- package/test/mapper.test.js +0 -17
- package/test/no-primary-key.test.js +79 -0
- package/test/schema.test.js +62 -1
- package/test/updateMany.test.js +4 -4
package/lib/entity.js
CHANGED
|
@@ -7,6 +7,12 @@ const {
|
|
|
7
7
|
tableName,
|
|
8
8
|
sanitizeLimit
|
|
9
9
|
} = require('./utils')
|
|
10
|
+
const { singularize } = require('inflected')
|
|
11
|
+
|
|
12
|
+
function lowerCaseFirst (str) {
|
|
13
|
+
str = str.toString()
|
|
14
|
+
return str.charAt(0).toLowerCase() + str.slice(1)
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relations, queries, autoTimestamp, schema, useSchemaInName, limitConfig) {
|
|
12
18
|
/* istanbul ignore next */ // Ignoring because this won't be fully covered by DB not supporting schemas (SQLite)
|
|
@@ -85,9 +91,9 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
let now
|
|
88
|
-
if (autoTimestamp && fields.
|
|
94
|
+
if (autoTimestamp && fields[autoTimestamp.updatedAt]) {
|
|
89
95
|
now = new Date()
|
|
90
|
-
input.
|
|
96
|
+
input[autoTimestamp.updatedAt] = now
|
|
91
97
|
}
|
|
92
98
|
if (hasPrimaryKeys) { // update
|
|
93
99
|
const res = await queries.updateOne(db, sql, table, schema, input, primaryKeys, fieldsToRetrieve)
|
|
@@ -100,10 +106,10 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
// insert
|
|
103
|
-
if (autoTimestamp && fields.
|
|
109
|
+
if (autoTimestamp && fields[autoTimestamp.createdAt]) {
|
|
104
110
|
/* istanbul ignore next */
|
|
105
111
|
now = now || new Date()
|
|
106
|
-
input.
|
|
112
|
+
input[autoTimestamp.createdAt] = now
|
|
107
113
|
}
|
|
108
114
|
const res = await queries.insertOne(db, sql, table, schema, input, primaryKeysTypes, fieldsToRetrieve)
|
|
109
115
|
return fixOutput(res)
|
|
@@ -118,11 +124,11 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
|
|
|
118
124
|
if (autoTimestamp) {
|
|
119
125
|
const now = new Date()
|
|
120
126
|
for (const input of inputs) {
|
|
121
|
-
if (fields.
|
|
122
|
-
input.
|
|
127
|
+
if (fields[autoTimestamp.createdAt]) {
|
|
128
|
+
input[autoTimestamp.createdAt] = now
|
|
123
129
|
}
|
|
124
|
-
if (fields.
|
|
125
|
-
input.updatedAt = now
|
|
130
|
+
if (fields[autoTimestamp.updatedAt]) {
|
|
131
|
+
input[autoTimestamp.updatedAt] = now
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
}
|
|
@@ -151,10 +157,9 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
|
|
|
151
157
|
throw new Error('Input not provided.')
|
|
152
158
|
}
|
|
153
159
|
const input = fixInput(args.input)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
input.updated_at = now
|
|
160
|
+
if (autoTimestamp && fields[autoTimestamp.updatedAt]) {
|
|
161
|
+
const now = new Date()
|
|
162
|
+
input[autoTimestamp.updatedAt] = now
|
|
158
163
|
}
|
|
159
164
|
const criteria = computeCriteria(args)
|
|
160
165
|
|
|
@@ -340,9 +345,20 @@ async function buildEntity (db, sql, log, table, queries, autoTimestamp, schema,
|
|
|
340
345
|
acc[column.column_name].enum = column.column_type.match(/'(.+?)'/g).map(enumValue => enumValue.slice(1, enumValue.length - 1))
|
|
341
346
|
}
|
|
342
347
|
|
|
343
|
-
if (autoTimestamp && (column.column_name ===
|
|
348
|
+
if (autoTimestamp && (column.column_name === autoTimestamp.createdAt || column.column_name === autoTimestamp.updatedAt)) {
|
|
344
349
|
acc[column.column_name].autoTimestamp = true
|
|
345
350
|
}
|
|
351
|
+
|
|
352
|
+
// To get generated information
|
|
353
|
+
/* istanbul ignore next */
|
|
354
|
+
if (db.isPg) {
|
|
355
|
+
acc[column.column_name].isGenerated = column.is_generated !== 'NEVER'
|
|
356
|
+
} else if (db.isSQLite) {
|
|
357
|
+
acc[column.column_name].isGenerated = column.is_generated === 'YES'
|
|
358
|
+
} else {
|
|
359
|
+
acc[column.column_name].isGenerated = column.is_generated.includes('GENERATED')
|
|
360
|
+
}
|
|
361
|
+
|
|
346
362
|
return acc
|
|
347
363
|
}, {})
|
|
348
364
|
|
|
@@ -363,6 +379,17 @@ async function buildEntity (db, sql, log, table, queries, autoTimestamp, schema,
|
|
|
363
379
|
const constraintsList = await queries.listConstraints(db, sql, table, schema)
|
|
364
380
|
const primaryKeys = new Set()
|
|
365
381
|
|
|
382
|
+
/* istanbul ignore next */
|
|
383
|
+
function checkSQLitePrimaryKey (constraint) {
|
|
384
|
+
if (db.isSQLite) {
|
|
385
|
+
const validTypes = ['integer', 'uuid', 'serial']
|
|
386
|
+
const pkType = fields[constraint.column_name].sqlType.toLowerCase()
|
|
387
|
+
if (!validTypes.includes(pkType)) {
|
|
388
|
+
throw new Error(`Invalid Primary Key type. Expected "integer", found "${pkType}"`)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
366
393
|
for (const constraint of constraintsList) {
|
|
367
394
|
const field = fields[constraint.column_name]
|
|
368
395
|
|
|
@@ -378,23 +405,54 @@ async function buildEntity (db, sql, log, table, queries, autoTimestamp, schema,
|
|
|
378
405
|
if (constraint.constraint_type === 'PRIMARY KEY') {
|
|
379
406
|
primaryKeys.add(constraint.column_name)
|
|
380
407
|
// Check for SQLite typeless PK
|
|
381
|
-
|
|
382
|
-
if (db.isSQLite) {
|
|
383
|
-
const validTypes = ['integer', 'uuid', 'serial']
|
|
384
|
-
const pkType = fields[constraint.column_name].sqlType.toLowerCase()
|
|
385
|
-
if (!validTypes.includes(pkType)) {
|
|
386
|
-
throw new Error(`Invalid Primary Key type. Expected "integer", found "${pkType}"`)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
408
|
+
checkSQLitePrimaryKey(constraint)
|
|
389
409
|
field.primaryKey = true
|
|
390
410
|
}
|
|
391
411
|
|
|
392
412
|
if (constraint.constraint_type === 'FOREIGN KEY') {
|
|
393
413
|
field.foreignKey = true
|
|
414
|
+
|
|
415
|
+
// we need to ignore for coverage here becasue cannot be covered with sqlite (no schema support)
|
|
416
|
+
// istanbul ignore next
|
|
417
|
+
const foreignEntityName = singularize(camelcase(useSchemaInName ? camelcase(`${constraint.foreign_table_schema} ${constraint.foreign_table_name}`) : constraint.foreign_table_name))
|
|
418
|
+
// istanbul ignore next
|
|
419
|
+
const entityName = singularize(camelcase(useSchemaInName ? camelcase(`${constraint.table_schema} ${constraint.table_name}`) : constraint.table_name))
|
|
420
|
+
// istanbul ignore next
|
|
421
|
+
const loweredTableWithSchemaName = lowerCaseFirst(useSchemaInName ? camelcase(`${constraint.table_schema} ${camelcase(constraint.table_name)}`) : camelcase(constraint.table_name))
|
|
422
|
+
constraint.loweredTableWithSchemaName = loweredTableWithSchemaName
|
|
423
|
+
constraint.foreignEntityName = foreignEntityName
|
|
424
|
+
constraint.entityName = entityName
|
|
394
425
|
currentRelations.push(constraint)
|
|
395
426
|
}
|
|
396
427
|
}
|
|
397
428
|
|
|
429
|
+
if (primaryKeys.size === 0) {
|
|
430
|
+
let found = false
|
|
431
|
+
for (const constraint of constraintsList) {
|
|
432
|
+
const field = fields[constraint.column_name]
|
|
433
|
+
|
|
434
|
+
/* istanbul ignore else */
|
|
435
|
+
if (constraint.constraint_type === 'UNIQUE') {
|
|
436
|
+
field.unique = true
|
|
437
|
+
|
|
438
|
+
/* istanbul ignore else */
|
|
439
|
+
if (!found) {
|
|
440
|
+
// Check for SQLite typeless PK
|
|
441
|
+
/* istanbul ignore next */
|
|
442
|
+
try {
|
|
443
|
+
checkSQLitePrimaryKey(constraint)
|
|
444
|
+
} catch {
|
|
445
|
+
continue
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
primaryKeys.add(constraint.column_name)
|
|
449
|
+
field.primaryKey = true
|
|
450
|
+
found = true
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
398
456
|
const entity = createMapper(db, sql, log, table, fields, primaryKeys, currentRelations, queries, autoTimestamp, schema, useSchemaInName, limitConfig)
|
|
399
457
|
entity.relations = currentRelations
|
|
400
458
|
|
|
@@ -23,7 +23,7 @@ async function listTables (db, sql, schemas) {
|
|
|
23
23
|
|
|
24
24
|
async function listColumns (db, sql, table, schema) {
|
|
25
25
|
const query = sql`
|
|
26
|
-
SELECT column_name as column_name, data_type as udt_name, is_nullable as is_nullable, column_type as column_type
|
|
26
|
+
SELECT column_name as column_name, data_type as udt_name, is_nullable as is_nullable, column_type as column_type, extra as is_generated
|
|
27
27
|
FROM information_schema.columns
|
|
28
28
|
WHERE table_name = ${table}
|
|
29
29
|
AND table_schema = ${schema}
|
|
@@ -33,7 +33,7 @@ async function listColumns (db, sql, table, schema) {
|
|
|
33
33
|
|
|
34
34
|
async function listConstraints (db, sql, table, schema) {
|
|
35
35
|
const query = sql`
|
|
36
|
-
SELECT TABLE_NAME as table_name, COLUMN_NAME as column_name, CONSTRAINT_TYPE as constraint_type, referenced_table_name AS foreign_table_name, referenced_column_name AS foreign_column_name
|
|
36
|
+
SELECT TABLE_NAME as table_name, TABLE_SCHEMA as table_schema, COLUMN_NAME as column_name, CONSTRAINT_TYPE as constraint_type, referenced_table_name AS foreign_table_name, referenced_table_schema AS foreign_table_schema, referenced_column_name AS foreign_column_name
|
|
37
37
|
FROM information_schema.table_constraints t
|
|
38
38
|
JOIN information_schema.key_column_usage k
|
|
39
39
|
USING (constraint_name, table_schema, table_name)
|
package/lib/queries/pg.js
CHANGED
|
@@ -44,7 +44,7 @@ module.exports.listTables = listTables
|
|
|
44
44
|
|
|
45
45
|
async function listColumns (db, sql, table, schema) {
|
|
46
46
|
return db.query(sql`
|
|
47
|
-
SELECT column_name, udt_name, is_nullable
|
|
47
|
+
SELECT column_name, udt_name, is_nullable, is_generated
|
|
48
48
|
FROM information_schema.columns
|
|
49
49
|
WHERE table_name = ${table}
|
|
50
50
|
AND table_schema = ${schema}
|
|
@@ -55,7 +55,7 @@ module.exports.listColumns = listColumns
|
|
|
55
55
|
|
|
56
56
|
async function listConstraints (db, sql, table, schema) {
|
|
57
57
|
const query = sql`
|
|
58
|
-
SELECT constraints.*, usage.*, usage2.table_name AS foreign_table_name, usage2.column_name AS foreign_column_name
|
|
58
|
+
SELECT constraints.*, usage.*, usage2.table_name AS foreign_table_name, usage2.column_name AS foreign_column_name, usage2.table_schema AS foreign_table_schema
|
|
59
59
|
FROM information_schema.table_constraints constraints
|
|
60
60
|
JOIN information_schema.key_column_usage usage
|
|
61
61
|
ON constraints.constraint_name = usage.constraint_name
|
package/lib/queries/sqlite.js
CHANGED
|
@@ -15,8 +15,10 @@ async function listTables (db, sql) {
|
|
|
15
15
|
module.exports.listTables = listTables
|
|
16
16
|
|
|
17
17
|
async function listColumns (db, sql, table) {
|
|
18
|
+
// pragma_table_info is not returning hidden column which tells if the column is generated or not
|
|
19
|
+
// therefore it is changed to pragma_table_xinfo
|
|
18
20
|
const columns = await db.query(sql`
|
|
19
|
-
SELECT * FROM
|
|
21
|
+
SELECT * FROM pragma_table_xinfo(${table})
|
|
20
22
|
`)
|
|
21
23
|
for (const column of columns) {
|
|
22
24
|
column.column_name = column.name
|
|
@@ -24,6 +26,8 @@ async function listColumns (db, sql, table) {
|
|
|
24
26
|
column.udt_name = column.type.replace(/^([^(]+).*/, '$1').toLowerCase()
|
|
25
27
|
// convert is_nullable
|
|
26
28
|
column.is_nullable = column.notnull === 0 && column.pk === 0 ? 'YES' : 'NO'
|
|
29
|
+
// convert hidden to is_generated
|
|
30
|
+
column.is_generated = (column.hidden === 2 || column.hidden === 3) ? 'YES' : 'NO'
|
|
27
31
|
}
|
|
28
32
|
return columns
|
|
29
33
|
}
|
|
@@ -45,6 +49,22 @@ async function listConstraints (db, sql, table) {
|
|
|
45
49
|
})
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
const indexes = await db.query(sql`
|
|
53
|
+
SELECT *
|
|
54
|
+
FROM pragma_index_list(${table}) as il
|
|
55
|
+
JOIN pragma_index_info(il.name) as ii
|
|
56
|
+
`)
|
|
57
|
+
|
|
58
|
+
for (const index of indexes) {
|
|
59
|
+
/* istanbul ignore else */
|
|
60
|
+
if (index.unique === 1) {
|
|
61
|
+
constraints.push({
|
|
62
|
+
column_name: index.name,
|
|
63
|
+
constraint_type: 'UNIQUE'
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
const foreignKeys = await db.query(sql`
|
|
49
69
|
SELECT *
|
|
50
70
|
FROM pragma_foreign_key_list(${table})
|
package/mapper.js
CHANGED
|
@@ -41,7 +41,15 @@ async function buildConnection (log, createConnectionPool, connectionString, poo
|
|
|
41
41
|
return db
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const defaultAutoTimestampFields = {
|
|
45
|
+
createdAt: 'created_at',
|
|
46
|
+
updatedAt: 'updated_at'
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10, ignore = {}, autoTimestamp = true, hooks = {}, schema, limit = {} }) {
|
|
50
|
+
if (typeof autoTimestamp === 'boolean' && autoTimestamp === true) {
|
|
51
|
+
autoTimestamp = defaultAutoTimestampFields
|
|
52
|
+
}
|
|
45
53
|
// TODO validate config using the schema
|
|
46
54
|
if (!connectionString) {
|
|
47
55
|
throw new Error('connectionString is required')
|
|
@@ -117,7 +125,8 @@ async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10,
|
|
|
117
125
|
const entity = await buildEntity(db, sql, log, table, queries, autoTimestamp, schema, useSchema, ignore[table] || {}, limit)
|
|
118
126
|
// Check for primary key of all entities
|
|
119
127
|
if (entity.primaryKeys.size === 0) {
|
|
120
|
-
|
|
128
|
+
log.warn({ table }, 'Cannot find any primary keys for table')
|
|
129
|
+
continue
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
entities[entity.singularName] = entity
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/sql-mapper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A data mapper utility for SQL databases",
|
|
5
5
|
"main": "mapper.js",
|
|
6
6
|
"repository": {
|
|
@@ -14,19 +14,19 @@
|
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/platformatic/platformatic#readme",
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"fastify": "^4.
|
|
17
|
+
"fastify": "^4.10.2",
|
|
18
18
|
"snazzy": "^9.0.0",
|
|
19
19
|
"standard": "^17.0.0",
|
|
20
|
-
"tap": "^16.
|
|
20
|
+
"tap": "^16.3.2",
|
|
21
21
|
"tsd": "^0.25.0"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@databases/mysql": "^5.2.
|
|
25
|
-
"@databases/pg": "^5.
|
|
24
|
+
"@databases/mysql": "^5.2.1",
|
|
25
|
+
"@databases/pg": "^5.4.1",
|
|
26
26
|
"@databases/sql": "^3.2.0",
|
|
27
27
|
"@databases/sqlite": "^4.0.2",
|
|
28
|
-
"camelcase": "^6.
|
|
29
|
-
"fastify-plugin": "^4.
|
|
28
|
+
"camelcase": "^6.3.0",
|
|
29
|
+
"fastify-plugin": "^4.4.0",
|
|
30
30
|
"inflected": "^2.1.0"
|
|
31
31
|
},
|
|
32
32
|
"tsd": {
|
package/test/entity.test.js
CHANGED
|
@@ -717,3 +717,155 @@ test('JSON type', { skip: !(isPg || isMysql8) }, async ({ teardown, same, equal,
|
|
|
717
717
|
}
|
|
718
718
|
}), [{ id: 2, config: { foo: 'bar', bar: 'foo' } }])
|
|
719
719
|
})
|
|
720
|
+
|
|
721
|
+
test('stored and virtual generated columns should return for SQLite', { skip: !(isSQLite) }, async ({ teardown, same }) => {
|
|
722
|
+
async function onDatabaseLoad (db, sql) {
|
|
723
|
+
await clear(db, sql)
|
|
724
|
+
teardown(() => db.dispose())
|
|
725
|
+
|
|
726
|
+
await db.query(sql`CREATE TABLE generated_test (
|
|
727
|
+
id INTEGER PRIMARY KEY,
|
|
728
|
+
test INTEGER,
|
|
729
|
+
test_stored INTEGER GENERATED ALWAYS AS (test*2) STORED,
|
|
730
|
+
test_virtual INTEGER GENERATED ALWAYS AS (test*4) VIRTUAL
|
|
731
|
+
);`)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const mapper = await connect({
|
|
735
|
+
connectionString: connInfo.connectionString,
|
|
736
|
+
log: fakeLogger,
|
|
737
|
+
onDatabaseLoad,
|
|
738
|
+
ignore: {},
|
|
739
|
+
hooks: {}
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
const generatedTest = mapper.entities.generatedTest
|
|
743
|
+
|
|
744
|
+
// save - new record
|
|
745
|
+
same(await generatedTest.save({
|
|
746
|
+
input: { test: 1 }
|
|
747
|
+
}), { id: 1, test: 1, testStored: 2, testVirtual: 4 })
|
|
748
|
+
|
|
749
|
+
// save - update
|
|
750
|
+
same(await generatedTest.save({
|
|
751
|
+
input: { id: 1, test: 2 }
|
|
752
|
+
}), { id: 1, test: 2, testStored: 4, testVirtual: 8 })
|
|
753
|
+
|
|
754
|
+
// insert
|
|
755
|
+
same(await generatedTest.insert({
|
|
756
|
+
inputs: [{ test: 4 }]
|
|
757
|
+
}), [{ id: 2, test: 4, testStored: 8, testVirtual: 16 }])
|
|
758
|
+
|
|
759
|
+
// updateMany
|
|
760
|
+
same(await generatedTest.updateMany({
|
|
761
|
+
where: {
|
|
762
|
+
id: {
|
|
763
|
+
eq: 2
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
input: {
|
|
767
|
+
test: 8
|
|
768
|
+
}
|
|
769
|
+
}), [{ id: 2, test: 8, testStored: 16, testVirtual: 32 }])
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
test('stored generated columns should return for pg', { skip: !(isPg) }, async ({ teardown, same }) => {
|
|
773
|
+
async function onDatabaseLoad (db, sql) {
|
|
774
|
+
await clear(db, sql)
|
|
775
|
+
teardown(() => db.dispose())
|
|
776
|
+
|
|
777
|
+
await db.query(sql`CREATE TABLE generated_test (
|
|
778
|
+
id SERIAL PRIMARY KEY,
|
|
779
|
+
test INTEGER,
|
|
780
|
+
test_stored INTEGER GENERATED ALWAYS AS (test*2) STORED
|
|
781
|
+
);`)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const mapper = await connect({
|
|
785
|
+
connectionString: connInfo.connectionString,
|
|
786
|
+
log: fakeLogger,
|
|
787
|
+
onDatabaseLoad,
|
|
788
|
+
ignore: {},
|
|
789
|
+
hooks: {}
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
const generatedTest = mapper.entities.generatedTest
|
|
793
|
+
|
|
794
|
+
// save - new record
|
|
795
|
+
same(await generatedTest.save({
|
|
796
|
+
input: { test: 1 }
|
|
797
|
+
}), { id: 1, test: 1, testStored: 2 })
|
|
798
|
+
|
|
799
|
+
// save - update
|
|
800
|
+
same(await generatedTest.save({
|
|
801
|
+
input: { id: 1, test: 2 }
|
|
802
|
+
}), { id: 1, test: 2, testStored: 4 })
|
|
803
|
+
|
|
804
|
+
// insert
|
|
805
|
+
same(await generatedTest.insert({
|
|
806
|
+
inputs: [{ test: 4 }]
|
|
807
|
+
}), [{ id: 2, test: 4, testStored: 8 }])
|
|
808
|
+
|
|
809
|
+
// updateMany
|
|
810
|
+
same(await generatedTest.updateMany({
|
|
811
|
+
where: {
|
|
812
|
+
id: {
|
|
813
|
+
eq: 2
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
input: {
|
|
817
|
+
test: 8
|
|
818
|
+
}
|
|
819
|
+
}), [{ id: 2, test: 8, testStored: 16 }])
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
test('stored and virtual generated columns should return for pg', { skip: (isPg || isSQLite) }, async ({ teardown, same }) => {
|
|
823
|
+
async function onDatabaseLoad (db, sql) {
|
|
824
|
+
await clear(db, sql)
|
|
825
|
+
teardown(() => db.dispose())
|
|
826
|
+
|
|
827
|
+
await db.query(sql`CREATE TABLE generated_test (
|
|
828
|
+
id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
829
|
+
test INTEGER,
|
|
830
|
+
test_stored INTEGER GENERATED ALWAYS AS (test*2) STORED,
|
|
831
|
+
test_virtual INTEGER GENERATED ALWAYS AS (test*4) VIRTUAL
|
|
832
|
+
);`)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const mapper = await connect({
|
|
836
|
+
connectionString: connInfo.connectionString,
|
|
837
|
+
log: fakeLogger,
|
|
838
|
+
onDatabaseLoad,
|
|
839
|
+
ignore: {},
|
|
840
|
+
hooks: {}
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
const generatedTest = mapper.entities.generatedTest
|
|
844
|
+
|
|
845
|
+
// save - new record
|
|
846
|
+
same(await generatedTest.save({
|
|
847
|
+
input: { test: 1 }
|
|
848
|
+
}), { id: 1, test: 1, testStored: 2, testVirtual: 4 })
|
|
849
|
+
|
|
850
|
+
// save - update
|
|
851
|
+
same(await generatedTest.save({
|
|
852
|
+
input: { id: 1, test: 2 }
|
|
853
|
+
}), { id: 1, test: 2, testStored: 4, testVirtual: 8 })
|
|
854
|
+
|
|
855
|
+
// insert
|
|
856
|
+
same(await generatedTest.insert({
|
|
857
|
+
inputs: [{ test: 4 }]
|
|
858
|
+
}), [{ id: 2, test: 4, testStored: 8, testVirtual: 16 }])
|
|
859
|
+
|
|
860
|
+
// updateMany
|
|
861
|
+
same(await generatedTest.updateMany({
|
|
862
|
+
where: {
|
|
863
|
+
id: {
|
|
864
|
+
eq: 2
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
input: {
|
|
868
|
+
test: 8
|
|
869
|
+
}
|
|
870
|
+
}), [{ id: 2, test: 8, testStored: 16, testVirtual: 32 }])
|
|
871
|
+
})
|
package/test/helper.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
// See https://node-postgres.com/features/types/
|
|
5
5
|
process.env.TZ = 'UTC'
|
|
6
6
|
|
|
7
|
-
const connInfo = {
|
|
7
|
+
const connInfo = {
|
|
8
|
+
autoTimestamp: {
|
|
9
|
+
createdAt: 'inserted_at',
|
|
10
|
+
updatedAt: 'updated_at'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
8
13
|
|
|
9
14
|
if (!process.env.DB || process.env.DB === 'postgresql') {
|
|
10
15
|
connInfo.connectionString = 'postgres://postgres:postgres@127.0.0.1/postgres'
|
|
@@ -89,4 +94,9 @@ module.exports.clear = async function (db, sql) {
|
|
|
89
94
|
await db.query(sql`DROP TABLE test2.pages`)
|
|
90
95
|
} catch (err) {
|
|
91
96
|
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await db.query(sql`DROP TABLE generated_test`)
|
|
100
|
+
} catch (err) {
|
|
101
|
+
}
|
|
92
102
|
}
|
package/test/mapper.test.js
CHANGED
|
@@ -193,20 +193,3 @@ test('missing connectionString', async ({ rejects }) => {
|
|
|
193
193
|
|
|
194
194
|
await rejects(app.ready(), /connectionString/)
|
|
195
195
|
})
|
|
196
|
-
|
|
197
|
-
test('throw if no primary keys', async ({ rejects, teardown }) => {
|
|
198
|
-
async function onDatabaseLoad (db, sql) {
|
|
199
|
-
await clear(db, sql)
|
|
200
|
-
|
|
201
|
-
await db.query(sql`CREATE TABLE pages (
|
|
202
|
-
title VARCHAR(255) NOT NULL
|
|
203
|
-
);`)
|
|
204
|
-
}
|
|
205
|
-
await rejects(connect({
|
|
206
|
-
connectionString: connInfo.connectionString,
|
|
207
|
-
log: fakeLogger,
|
|
208
|
-
onDatabaseLoad,
|
|
209
|
-
ignore: {},
|
|
210
|
-
hooks: {}
|
|
211
|
-
}))
|
|
212
|
-
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
|
|
5
|
+
const { clear, connInfo, isMysql8 } = require('./helper')
|
|
6
|
+
const { connect } = require('..')
|
|
7
|
+
const fakeLogger = {
|
|
8
|
+
trace: () => {},
|
|
9
|
+
warn: () => {},
|
|
10
|
+
error: () => {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('unique key', async ({ equal, not, same, teardown }) => {
|
|
14
|
+
async function onDatabaseLoad (db, sql) {
|
|
15
|
+
await clear(db, sql)
|
|
16
|
+
teardown(() => db.dispose())
|
|
17
|
+
|
|
18
|
+
const table = sql`
|
|
19
|
+
CREATE TABLE pages (
|
|
20
|
+
xx INTEGER DEFAULT NULL UNIQUE,
|
|
21
|
+
name varchar(75) DEFAULT NULL UNIQUE
|
|
22
|
+
);
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
await db.query(table)
|
|
26
|
+
}
|
|
27
|
+
const mapper = await connect({
|
|
28
|
+
connectionString: connInfo.connectionString,
|
|
29
|
+
log: fakeLogger,
|
|
30
|
+
onDatabaseLoad,
|
|
31
|
+
ignore: {},
|
|
32
|
+
hooks: {}
|
|
33
|
+
})
|
|
34
|
+
const pageEntity = mapper.entities.page
|
|
35
|
+
not(pageEntity, undefined)
|
|
36
|
+
equal(pageEntity.name, 'Page')
|
|
37
|
+
equal(pageEntity.singularName, 'page')
|
|
38
|
+
equal(pageEntity.pluralName, 'pages')
|
|
39
|
+
if (isMysql8) {
|
|
40
|
+
same(pageEntity.primaryKeys, new Set(['name']))
|
|
41
|
+
equal(pageEntity.camelCasedFields.name.primaryKey, true)
|
|
42
|
+
} else {
|
|
43
|
+
same(pageEntity.primaryKeys, new Set(['xx']))
|
|
44
|
+
equal(pageEntity.camelCasedFields.xx.primaryKey, true)
|
|
45
|
+
}
|
|
46
|
+
equal(pageEntity.camelCasedFields.xx.unique, true)
|
|
47
|
+
equal(pageEntity.camelCasedFields.name.unique, true)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('no key', async ({ same, teardown, pass, equal, plan }) => {
|
|
51
|
+
plan(3)
|
|
52
|
+
async function onDatabaseLoad (db, sql) {
|
|
53
|
+
await clear(db, sql)
|
|
54
|
+
teardown(() => db.dispose())
|
|
55
|
+
|
|
56
|
+
const table = sql`
|
|
57
|
+
CREATE TABLE pages (
|
|
58
|
+
xx INTEGER DEFAULT NULL,
|
|
59
|
+
name varchar(75) DEFAULT NULL
|
|
60
|
+
);
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
await db.query(table)
|
|
64
|
+
}
|
|
65
|
+
const log = {
|
|
66
|
+
trace: () => {},
|
|
67
|
+
warn: (obj, str) => {
|
|
68
|
+
same(obj, { table: 'pages' })
|
|
69
|
+
equal(str, 'Cannot find any primary keys for table')
|
|
70
|
+
},
|
|
71
|
+
error: () => {}
|
|
72
|
+
}
|
|
73
|
+
const mapper = await connect({
|
|
74
|
+
connectionString: connInfo.connectionString,
|
|
75
|
+
log,
|
|
76
|
+
onDatabaseLoad
|
|
77
|
+
})
|
|
78
|
+
same(mapper.entities, {})
|
|
79
|
+
})
|
package/test/schema.test.js
CHANGED
|
@@ -174,7 +174,6 @@ test('[pg] if schema is empty array, should find entities only in default \'publ
|
|
|
174
174
|
hooks: {},
|
|
175
175
|
schema: []
|
|
176
176
|
})
|
|
177
|
-
|
|
178
177
|
equal(Object.keys(mapper.entities).length, 1)
|
|
179
178
|
const pageEntity = mapper.entities.page
|
|
180
179
|
equal(pageEntity.name, 'Page')
|
|
@@ -318,3 +317,65 @@ test('addEntityHooks in entities with schema', { skip: isSQLite }, async ({ pass
|
|
|
318
317
|
await entity.insert({ inputs: [{ title: 'hello' }, { title: 'world' }], fields: ['id', 'title'] })
|
|
319
318
|
end()
|
|
320
319
|
})
|
|
320
|
+
|
|
321
|
+
test('uses tables from different schemas with FK', { skip: isSQLite }, async ({ pass, teardown, equal }) => {
|
|
322
|
+
async function onDatabaseLoad (db, sql) {
|
|
323
|
+
await clear(db, sql)
|
|
324
|
+
teardown(() => db.dispose())
|
|
325
|
+
|
|
326
|
+
await db.query(sql`CREATE SCHEMA IF NOT EXISTS test1;`)
|
|
327
|
+
if (isMysql || isMysql8) {
|
|
328
|
+
await db.query(sql`CREATE TABLE IF NOT EXISTS \`test1\`.\`pages\` (
|
|
329
|
+
id SERIAL PRIMARY KEY,
|
|
330
|
+
title VARCHAR(255) NOT NULL
|
|
331
|
+
);`)
|
|
332
|
+
} else {
|
|
333
|
+
await db.query(sql`CREATE TABLE IF NOT EXISTS "test1"."pages" (
|
|
334
|
+
id SERIAL PRIMARY KEY,
|
|
335
|
+
title VARCHAR(255) NOT NULL
|
|
336
|
+
);`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await db.query(sql`CREATE SCHEMA IF NOT EXISTS test2;`)
|
|
340
|
+
|
|
341
|
+
if (isMysql || isMysql8) {
|
|
342
|
+
await db.query(sql`CREATE TABLE IF NOT EXISTS \`test2\`.\`users\` (
|
|
343
|
+
id SERIAL PRIMARY KEY,
|
|
344
|
+
username VARCHAR(255) NOT NULL,
|
|
345
|
+
page_id BIGINT UNSIGNED,
|
|
346
|
+
FOREIGN KEY(page_id) REFERENCES test1.pages(id) ON DELETE CASCADE
|
|
347
|
+
);`)
|
|
348
|
+
} else {
|
|
349
|
+
await db.query(sql`CREATE TABLE IF NOT EXISTS "test2"."users" (
|
|
350
|
+
id SERIAL PRIMARY KEY,
|
|
351
|
+
username VARCHAR(255) NOT NULL,
|
|
352
|
+
page_id integer REFERENCES test1.pages(id)
|
|
353
|
+
);`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const mapper = await connect({
|
|
357
|
+
connectionString: connInfo.connectionString,
|
|
358
|
+
log: fakeLogger,
|
|
359
|
+
onDatabaseLoad,
|
|
360
|
+
ignore: {},
|
|
361
|
+
hooks: {},
|
|
362
|
+
schema: ['test1', 'test2']
|
|
363
|
+
})
|
|
364
|
+
const pageEntity = mapper.entities.test1Page
|
|
365
|
+
equal(pageEntity.name, 'Test1Page')
|
|
366
|
+
equal(pageEntity.singularName, 'test1Page')
|
|
367
|
+
equal(pageEntity.pluralName, 'test1Pages')
|
|
368
|
+
equal(pageEntity.schema, 'test1')
|
|
369
|
+
equal(pageEntity.relations.length, 0)
|
|
370
|
+
|
|
371
|
+
const userEntity = mapper.entities.test2User
|
|
372
|
+
equal(userEntity.name, 'Test2User')
|
|
373
|
+
equal(userEntity.singularName, 'test2User')
|
|
374
|
+
equal(userEntity.pluralName, 'test2Users')
|
|
375
|
+
equal(userEntity.schema, 'test2')
|
|
376
|
+
equal(userEntity.relations.length, 1)
|
|
377
|
+
const userRelation = userEntity.relations[0]
|
|
378
|
+
equal(userRelation.foreignEntityName, 'test1Page')
|
|
379
|
+
equal(userRelation.entityName, 'test2User')
|
|
380
|
+
pass()
|
|
381
|
+
})
|
package/test/updateMany.test.js
CHANGED
|
@@ -248,7 +248,7 @@ test('updateMany successful and update updated_at', async ({ pass, teardown, sam
|
|
|
248
248
|
title VARCHAR(42),
|
|
249
249
|
long_text TEXT,
|
|
250
250
|
counter INTEGER,
|
|
251
|
-
|
|
251
|
+
created_at TIMESTAMP,
|
|
252
252
|
updated_at TIMESTAMP
|
|
253
253
|
);`)
|
|
254
254
|
} else if (isMysql) {
|
|
@@ -257,7 +257,7 @@ test('updateMany successful and update updated_at', async ({ pass, teardown, sam
|
|
|
257
257
|
title VARCHAR(42),
|
|
258
258
|
long_text TEXT,
|
|
259
259
|
counter INTEGER,
|
|
260
|
-
|
|
260
|
+
created_at TIMESTAMP NULL DEFAULT NULL,
|
|
261
261
|
updated_at TIMESTAMP NULL DEFAULT NULL
|
|
262
262
|
);`)
|
|
263
263
|
} else {
|
|
@@ -266,7 +266,7 @@ test('updateMany successful and update updated_at', async ({ pass, teardown, sam
|
|
|
266
266
|
title VARCHAR(42),
|
|
267
267
|
long_text TEXT,
|
|
268
268
|
counter INTEGER,
|
|
269
|
-
|
|
269
|
+
created_at TIMESTAMP,
|
|
270
270
|
updated_at TIMESTAMP
|
|
271
271
|
);`)
|
|
272
272
|
}
|
|
@@ -313,6 +313,6 @@ test('updateMany successful and update updated_at', async ({ pass, teardown, sam
|
|
|
313
313
|
|
|
314
314
|
const updatedPost3 = (await entity.find({ where: { id: { eq: '3' } } }))[0]
|
|
315
315
|
same(updatedPost3.title, 'Updated title')
|
|
316
|
-
same(createdPost3.
|
|
316
|
+
same(createdPost3.createdAt, updatedPost3.createdAt)
|
|
317
317
|
notSame(createdPost3.updatedAt, updatedPost3.updatedAt)
|
|
318
318
|
})
|