@platformatic/sql-mapper 0.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/.nyc_output/0208d41a-48bc-4675-a861-0475eb461a17.json +1 -0
- package/.nyc_output/588169a6-88e9-4949-af5a-631822d7dc42.json +1 -0
- package/.nyc_output/5bbdf331-cd01-4869-9d54-3d708610224a.json +1 -0
- package/.nyc_output/6d7c60ad-a404-4a1d-af86-8a334cff5f02.json +1 -0
- package/.nyc_output/8dae7e8c-5022-4a0c-a5b3-bf547f97961b.json +1 -0
- package/.nyc_output/f63bf7c5-4f58-4b46-a822-6f5ccf5a54a8.json +1 -0
- package/.nyc_output/processinfo/0208d41a-48bc-4675-a861-0475eb461a17.json +1 -0
- package/.nyc_output/processinfo/588169a6-88e9-4949-af5a-631822d7dc42.json +1 -0
- package/.nyc_output/processinfo/5bbdf331-cd01-4869-9d54-3d708610224a.json +1 -0
- package/.nyc_output/processinfo/6d7c60ad-a404-4a1d-af86-8a334cff5f02.json +1 -0
- package/.nyc_output/processinfo/8dae7e8c-5022-4a0c-a5b3-bf547f97961b.json +1 -0
- package/.nyc_output/processinfo/f63bf7c5-4f58-4b46-a822-6f5ccf5a54a8.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/.taprc +1 -0
- package/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +13 -0
- package/lib/entity.js +287 -0
- package/lib/queries/index.js +23 -0
- package/lib/queries/mariadb.js +11 -0
- package/lib/queries/mysql-shared.js +62 -0
- package/lib/queries/mysql.js +104 -0
- package/lib/queries/pg.js +79 -0
- package/lib/queries/shared.js +100 -0
- package/lib/queries/sqlite.js +169 -0
- package/lib/utils.js +14 -0
- package/mapper.d.ts +308 -0
- package/mapper.js +155 -0
- package/package.json +44 -0
- package/test/entity.test.js +344 -0
- package/test/helper.js +66 -0
- package/test/hooks.test.js +325 -0
- package/test/inserted_at_updated_at.test.js +132 -0
- package/test/mapper.test.js +288 -0
- package/test/types/mapper.test-d.ts +64 -0
- package/test/where.test.js +316 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('crypto')
|
|
4
|
+
|
|
5
|
+
async function listTables (db, sql) {
|
|
6
|
+
const tables = await db.query(sql`
|
|
7
|
+
SELECT name FROM sqlite_master
|
|
8
|
+
WHERE type='table'
|
|
9
|
+
`)
|
|
10
|
+
return tables.map(t => t.name)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
module.exports.listTables = listTables
|
|
14
|
+
|
|
15
|
+
async function listColumns (db, sql, table) {
|
|
16
|
+
const columns = await db.query(sql`
|
|
17
|
+
SELECT * FROM pragma_table_info(${table})
|
|
18
|
+
`)
|
|
19
|
+
for (const column of columns) {
|
|
20
|
+
column.column_name = column.name
|
|
21
|
+
// convert varchar(42) in varchar
|
|
22
|
+
column.udt_name = column.type.replace(/^([^(]+).*/, '$1').toLowerCase()
|
|
23
|
+
// convert is_nullable
|
|
24
|
+
column.is_nullable = column.notnull === 0 && column.pk === 0 ? 'YES' : 'NO'
|
|
25
|
+
}
|
|
26
|
+
return columns
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports.listColumns = listColumns
|
|
30
|
+
|
|
31
|
+
async function listConstraints (db, sql, table) {
|
|
32
|
+
const constraints = []
|
|
33
|
+
const pks = await db.query(sql`
|
|
34
|
+
SELECT *
|
|
35
|
+
FROM pragma_table_info(${table})
|
|
36
|
+
WHERE pk > 0
|
|
37
|
+
`)
|
|
38
|
+
|
|
39
|
+
if (pks.length > 1) {
|
|
40
|
+
throw new Error(`Table ${table} has ${pks.length} primary keys`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (pks.length === 1) {
|
|
44
|
+
constraints.push({
|
|
45
|
+
column_name: pks[0].name,
|
|
46
|
+
constraint_type: 'PRIMARY KEY'
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const foreignKeys = await db.query(sql`
|
|
51
|
+
SELECT *
|
|
52
|
+
FROM pragma_foreign_key_list(${table})
|
|
53
|
+
`)
|
|
54
|
+
|
|
55
|
+
for (const foreignKey of foreignKeys) {
|
|
56
|
+
constraints.push({
|
|
57
|
+
table_name: table,
|
|
58
|
+
column_name: foreignKey.from,
|
|
59
|
+
constraint_type: 'FOREIGN KEY',
|
|
60
|
+
foreign_table_name: foreignKey.table,
|
|
61
|
+
foreign_column_name: foreignKey.to
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return constraints
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports.listConstraints = listConstraints
|
|
68
|
+
|
|
69
|
+
async function insertOne (db, sql, table, input, primaryKey, useUUID, fieldsToRetrieve) {
|
|
70
|
+
const keysToSql = Object.keys(input).map((key) => sql.ident(key))
|
|
71
|
+
keysToSql.push(sql.ident(primaryKey))
|
|
72
|
+
const keys = sql.join(
|
|
73
|
+
keysToSql,
|
|
74
|
+
sql`, `
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const valuesToSql = Object.keys(input).map((key) => {
|
|
78
|
+
return sql.value(input[key])
|
|
79
|
+
})
|
|
80
|
+
let primaryKeyValue
|
|
81
|
+
// TODO add test for this
|
|
82
|
+
if (useUUID) {
|
|
83
|
+
primaryKeyValue = randomUUID()
|
|
84
|
+
valuesToSql.push(sql.value(primaryKeyValue))
|
|
85
|
+
} else {
|
|
86
|
+
valuesToSql.push(sql.value(null))
|
|
87
|
+
}
|
|
88
|
+
const values = sql.join(
|
|
89
|
+
valuesToSql,
|
|
90
|
+
sql`, `
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const insert = sql`
|
|
94
|
+
INSERT INTO ${sql.ident(table)} (${keys})
|
|
95
|
+
VALUES(${values})
|
|
96
|
+
`
|
|
97
|
+
await db.query(insert)
|
|
98
|
+
|
|
99
|
+
if (!useUUID) {
|
|
100
|
+
const res2 = await db.query(sql`
|
|
101
|
+
SELECT last_insert_rowid()
|
|
102
|
+
`)
|
|
103
|
+
|
|
104
|
+
primaryKeyValue = res2[0]['last_insert_rowid()']
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const res = await db.query(sql`
|
|
108
|
+
SELECT ${sql.join(fieldsToRetrieve, sql`, `)}
|
|
109
|
+
FROM ${sql.ident(table)}
|
|
110
|
+
WHERE ${sql.ident(primaryKey)} = ${sql.value(primaryKeyValue)}
|
|
111
|
+
`)
|
|
112
|
+
|
|
113
|
+
return res[0]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports.insertOne = insertOne
|
|
117
|
+
|
|
118
|
+
async function updateOne (db, sql, table, input, primaryKey, fieldsToRetrieve) {
|
|
119
|
+
const pairs = Object.keys(input).map((key) => {
|
|
120
|
+
const value = input[key]
|
|
121
|
+
return sql`${sql.ident(key)} = ${value}`
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const update = sql`
|
|
125
|
+
UPDATE ${sql.ident(table)}
|
|
126
|
+
SET ${sql.join(pairs, sql`, `)}
|
|
127
|
+
WHERE ${sql.ident(primaryKey)} = ${sql.value(input[primaryKey])}
|
|
128
|
+
`
|
|
129
|
+
await db.query(update)
|
|
130
|
+
|
|
131
|
+
const select = sql`
|
|
132
|
+
SELECT ${sql.join(fieldsToRetrieve, sql`, `)}
|
|
133
|
+
FROM ${sql.ident(table)}
|
|
134
|
+
WHERE ${sql.ident(primaryKey)} = ${sql.value(input[primaryKey])}
|
|
135
|
+
`
|
|
136
|
+
const res = await db.query(select)
|
|
137
|
+
return res[0]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports.updateOne = updateOne
|
|
141
|
+
|
|
142
|
+
async function deleteAll (db, sql, table, criteria, fieldsToRetrieve) {
|
|
143
|
+
let query = sql`
|
|
144
|
+
SELECT ${sql.join(fieldsToRetrieve, sql`, `)}
|
|
145
|
+
FROM ${sql.ident(table)}
|
|
146
|
+
`
|
|
147
|
+
|
|
148
|
+
/* istanbul ignore else */
|
|
149
|
+
if (criteria.length > 0) {
|
|
150
|
+
query = sql`${query} WHERE ${sql.join(criteria, sql` AND `)}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await db.query(query)
|
|
154
|
+
|
|
155
|
+
query = sql`
|
|
156
|
+
DELETE FROM ${sql.ident(table)}
|
|
157
|
+
`
|
|
158
|
+
|
|
159
|
+
/* istanbul ignore else */
|
|
160
|
+
if (criteria.length > 0) {
|
|
161
|
+
query = sql`${query} WHERE ${sql.join(criteria, sql` AND `)}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await db.query(query)
|
|
165
|
+
|
|
166
|
+
return data
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports.deleteAll = deleteAll
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { singularize } = require('inflected')
|
|
4
|
+
const camelcase = require('camelcase')
|
|
5
|
+
|
|
6
|
+
function toSingular (str) {
|
|
7
|
+
str = camelcase(singularize(str))
|
|
8
|
+
str = str[0].toUpperCase() + str.slice(1)
|
|
9
|
+
return str
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
toSingular
|
|
14
|
+
}
|
package/mapper.d.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { FastifyPluginAsync } from 'fastify'
|
|
2
|
+
import { SQL, SQLQuery } from '@databases/sql'
|
|
3
|
+
|
|
4
|
+
interface ILogger {
|
|
5
|
+
trace(): any,
|
|
6
|
+
error(): any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Database {
|
|
10
|
+
/**
|
|
11
|
+
* An option that is true if a Postgres database is used.
|
|
12
|
+
*/
|
|
13
|
+
isPg?: boolean,
|
|
14
|
+
/**
|
|
15
|
+
* An option that is true if a MariaDB database is used.
|
|
16
|
+
*/
|
|
17
|
+
isMariaDB?: boolean,
|
|
18
|
+
/**
|
|
19
|
+
* An option that is true if a MySQL database is used.
|
|
20
|
+
*/
|
|
21
|
+
isMySQL?: boolean,
|
|
22
|
+
/**
|
|
23
|
+
* An option that is true if a SQLite database is used.
|
|
24
|
+
*/
|
|
25
|
+
isSQLite?: boolean,
|
|
26
|
+
/**
|
|
27
|
+
* Run an SQL Query and get a promise for an array of results. If your query contains multiple statements, only the results of the final statement are returned.
|
|
28
|
+
*/
|
|
29
|
+
query(query: SQLQuery): Promise<any[]>,
|
|
30
|
+
/**
|
|
31
|
+
* Dispose the connection. Once this is called, any subsequent queries will fail.
|
|
32
|
+
*/
|
|
33
|
+
dispose(): Promise<void>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DBEntityField {
|
|
37
|
+
/**
|
|
38
|
+
* Field type in the database.
|
|
39
|
+
*/
|
|
40
|
+
sqlType: string,
|
|
41
|
+
/**
|
|
42
|
+
* Camel cased field name.
|
|
43
|
+
*/
|
|
44
|
+
camelcase: string,
|
|
45
|
+
/**
|
|
46
|
+
* An option that is true if field is a primary key.
|
|
47
|
+
*/
|
|
48
|
+
primaryKey?: boolean,
|
|
49
|
+
/**
|
|
50
|
+
* An option that is true if field is a foreignKey key.
|
|
51
|
+
*/
|
|
52
|
+
foreignKey?: boolean,
|
|
53
|
+
/**
|
|
54
|
+
* An option that is true if field is nullable.
|
|
55
|
+
*/
|
|
56
|
+
isNullable: boolean,
|
|
57
|
+
/**
|
|
58
|
+
* An option that is true if auto timestamp enabled for this field.
|
|
59
|
+
*/
|
|
60
|
+
autoTimestamp?: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WhereCondition {
|
|
64
|
+
[columnName: string]: {
|
|
65
|
+
/**
|
|
66
|
+
* Equal to value.
|
|
67
|
+
*/
|
|
68
|
+
eq?: string,
|
|
69
|
+
/**
|
|
70
|
+
* Not equal to value.
|
|
71
|
+
*/
|
|
72
|
+
neq?: string,
|
|
73
|
+
/**
|
|
74
|
+
* Greater than value.
|
|
75
|
+
*/
|
|
76
|
+
gr?: any,
|
|
77
|
+
/**
|
|
78
|
+
* Greater than or equal to value.
|
|
79
|
+
*/
|
|
80
|
+
gte?: any,
|
|
81
|
+
/**
|
|
82
|
+
* Less than value.
|
|
83
|
+
*/
|
|
84
|
+
lt?: any,
|
|
85
|
+
/**
|
|
86
|
+
* Less than or equal to value.
|
|
87
|
+
*/
|
|
88
|
+
lte?: any,
|
|
89
|
+
/**
|
|
90
|
+
* In values.
|
|
91
|
+
*/
|
|
92
|
+
in?: any[],
|
|
93
|
+
/**
|
|
94
|
+
* Not in values.
|
|
95
|
+
*/
|
|
96
|
+
nin?: any[]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface Find<EntityFields> {
|
|
101
|
+
(options?: {
|
|
102
|
+
/**
|
|
103
|
+
* SQL where condition.
|
|
104
|
+
*/
|
|
105
|
+
where?: WhereCondition,
|
|
106
|
+
/**
|
|
107
|
+
* List of fields to be returned for each object
|
|
108
|
+
*/
|
|
109
|
+
fields?: string[],
|
|
110
|
+
/**
|
|
111
|
+
* Entity fields to order by.
|
|
112
|
+
*/
|
|
113
|
+
orderBy?: Array<{ field: string, direction: 'asc' | 'desc' }>,
|
|
114
|
+
/**
|
|
115
|
+
* Number of entities to select.
|
|
116
|
+
*/
|
|
117
|
+
limit?: number,
|
|
118
|
+
/**
|
|
119
|
+
* Number of entities to skip.
|
|
120
|
+
*/
|
|
121
|
+
offset?: number,
|
|
122
|
+
}): Promise<Partial<EntityFields>[]>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface Insert<EntityFields> {
|
|
126
|
+
(options: {
|
|
127
|
+
/**
|
|
128
|
+
* Entities to insert.
|
|
129
|
+
*/
|
|
130
|
+
inputs: EntityFields[],
|
|
131
|
+
/**
|
|
132
|
+
* List of fields to be returned for each object
|
|
133
|
+
*/
|
|
134
|
+
fields?: string[]
|
|
135
|
+
}): Promise<Partial<EntityFields>[]>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface Save<EntityFields> {
|
|
139
|
+
(options: {
|
|
140
|
+
/**
|
|
141
|
+
* Entity to save.
|
|
142
|
+
*/
|
|
143
|
+
input: EntityFields,
|
|
144
|
+
/**
|
|
145
|
+
* List of fields to be returned for each object
|
|
146
|
+
*/
|
|
147
|
+
fields?: string[]
|
|
148
|
+
}): Promise<Partial<EntityFields>>
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface Delete<EntityFields> {
|
|
152
|
+
(options?: {
|
|
153
|
+
/**
|
|
154
|
+
* SQL where condition.
|
|
155
|
+
*/
|
|
156
|
+
where: WhereCondition,
|
|
157
|
+
/**
|
|
158
|
+
* List of fields to be returned for each object
|
|
159
|
+
*/
|
|
160
|
+
fields: string[]
|
|
161
|
+
}): Promise<Partial<EntityFields>[]>,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface Entity<EntityFields = any> {
|
|
165
|
+
/**
|
|
166
|
+
* The origin name of the database entity.
|
|
167
|
+
*/
|
|
168
|
+
name: string,
|
|
169
|
+
/**
|
|
170
|
+
* The name of the database object in the singular.
|
|
171
|
+
*/
|
|
172
|
+
singularName: string,
|
|
173
|
+
/**
|
|
174
|
+
* The plural name of the database entity.
|
|
175
|
+
*/
|
|
176
|
+
pluralName: string,
|
|
177
|
+
/**
|
|
178
|
+
* The primary key of the database entity.
|
|
179
|
+
*/
|
|
180
|
+
primaryKey: string,
|
|
181
|
+
/**
|
|
182
|
+
* The table of the database entity.
|
|
183
|
+
*/
|
|
184
|
+
table: string,
|
|
185
|
+
/**
|
|
186
|
+
* Fields of the database entity.
|
|
187
|
+
*/
|
|
188
|
+
fields: { [columnName: string]: DBEntityField },
|
|
189
|
+
/**
|
|
190
|
+
* Camel cased fields of the database entity.
|
|
191
|
+
*/
|
|
192
|
+
camelCasedFields: { [columnName: string]: DBEntityField },
|
|
193
|
+
/**
|
|
194
|
+
* Relations with other database entities.
|
|
195
|
+
*/
|
|
196
|
+
relations: any[],
|
|
197
|
+
/**
|
|
198
|
+
* Converts entities fields names to database column names.
|
|
199
|
+
*/
|
|
200
|
+
fixInput(input: { [columnName: string]: any }): { [columnName: string]: any },
|
|
201
|
+
/**
|
|
202
|
+
* Converts database column names to entities fields names.
|
|
203
|
+
*/
|
|
204
|
+
fixOutput(input: { [columnName: string]: any }): { [columnName: string]: any },
|
|
205
|
+
/**
|
|
206
|
+
* Selects matching entities from the database.
|
|
207
|
+
*/
|
|
208
|
+
find: Find<EntityFields>,
|
|
209
|
+
/**
|
|
210
|
+
* Inserts entities to the database.
|
|
211
|
+
*/
|
|
212
|
+
insert: Insert<EntityFields>,
|
|
213
|
+
/**
|
|
214
|
+
* Saves entity to the database.
|
|
215
|
+
*/
|
|
216
|
+
save: Save<EntityFields>,
|
|
217
|
+
/**
|
|
218
|
+
* Deletes entities from the database.
|
|
219
|
+
*/
|
|
220
|
+
delete: Delete<EntityFields>,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
export interface EntityHooks<EntityFields = any> {
|
|
225
|
+
[entityName: string]: {
|
|
226
|
+
find?: Find<EntityFields>,
|
|
227
|
+
insert?: Insert<EntityFields>,
|
|
228
|
+
save?: Save<EntityFields>,
|
|
229
|
+
delete?: Delete<EntityFields>,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface SQLMapperPluginOptions {
|
|
234
|
+
/**
|
|
235
|
+
* Database connection string.
|
|
236
|
+
*/
|
|
237
|
+
connectionString: string,
|
|
238
|
+
/**
|
|
239
|
+
* Set to true to enable auto timestamping for updated_at and inserted_at fields.
|
|
240
|
+
*/
|
|
241
|
+
autoTimestamp?: boolean,
|
|
242
|
+
/**
|
|
243
|
+
* A logger object (like [Pino](https://getpino.io))
|
|
244
|
+
*/
|
|
245
|
+
log?: ILogger,
|
|
246
|
+
/**
|
|
247
|
+
* Database table to ignore when mapping to entities.
|
|
248
|
+
*/
|
|
249
|
+
ignore?: {
|
|
250
|
+
[tableName: string]: {
|
|
251
|
+
[columnName: string]: boolean
|
|
252
|
+
} | boolean
|
|
253
|
+
},
|
|
254
|
+
/**
|
|
255
|
+
* For each entity name (like `Page`) you can customize any of the entity API function. Your custom function will receive the original function as first parameter, and then all the other parameters passed to it.
|
|
256
|
+
*/
|
|
257
|
+
hooks?: EntityHooks,
|
|
258
|
+
/**
|
|
259
|
+
* An async function that is called after the connection is established.
|
|
260
|
+
*/
|
|
261
|
+
onDatabaseLoad?(db: Database, sql: SQL): any,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface Entities {
|
|
265
|
+
[entityName: string]: Entity
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface SQLMapperPluginInterface {
|
|
269
|
+
/**
|
|
270
|
+
* A Database abstraction layer from [@Databases](https://www.atdatabases.org/)
|
|
271
|
+
*/
|
|
272
|
+
db: Database,
|
|
273
|
+
/**
|
|
274
|
+
* The SQL builder from [@Databases](https://www.atdatabases.org/)
|
|
275
|
+
*/
|
|
276
|
+
sql: SQL,
|
|
277
|
+
/**
|
|
278
|
+
* An object containing a key for each table found in the schema, with basic CRUD operations. See [entity.md](./entity.md) for details.
|
|
279
|
+
*/
|
|
280
|
+
entities: Entities,
|
|
281
|
+
/**
|
|
282
|
+
* Adds hooks to the entity.
|
|
283
|
+
*/
|
|
284
|
+
addEntityHooks(entityName: string, hooks: EntityHooks): any
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
declare module 'fastify' {
|
|
288
|
+
interface FastifyInstance {
|
|
289
|
+
platformatic: SQLMapperPluginInterface
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Connects to the database and maps the tables to entities.
|
|
295
|
+
*/
|
|
296
|
+
export function connect(options: SQLMapperPluginOptions): Promise<SQLMapperPluginInterface>
|
|
297
|
+
/**
|
|
298
|
+
* Fastify plugin that connects to the database and maps the tables to entities.
|
|
299
|
+
*/
|
|
300
|
+
export const plugin: FastifyPluginAsync<SQLMapperPluginOptions>
|
|
301
|
+
export default plugin
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* An object that contains utility functions.
|
|
305
|
+
*/
|
|
306
|
+
export module utils {
|
|
307
|
+
export function toSingular (str: string): string
|
|
308
|
+
}
|
package/mapper.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const buildEntity = require('./lib/entity')
|
|
4
|
+
const queriesFactory = require('./lib/queries')
|
|
5
|
+
const fp = require('fastify-plugin')
|
|
6
|
+
|
|
7
|
+
// Ignore the function as it is only used only for MySQL and PostgreSQL
|
|
8
|
+
/* istanbul ignore next */
|
|
9
|
+
async function buildConnection (log, createConnectionPool, connectionString) {
|
|
10
|
+
const db = await createConnectionPool({
|
|
11
|
+
connectionString,
|
|
12
|
+
bigIntMode: 'string',
|
|
13
|
+
onQueryStart: (_query, { text, values }) => {
|
|
14
|
+
log.trace({
|
|
15
|
+
query: {
|
|
16
|
+
text,
|
|
17
|
+
values
|
|
18
|
+
}
|
|
19
|
+
}, 'start query')
|
|
20
|
+
},
|
|
21
|
+
onQueryResults: (_query, { text }, results) => {
|
|
22
|
+
log.trace({
|
|
23
|
+
query: {
|
|
24
|
+
text,
|
|
25
|
+
results: results.length
|
|
26
|
+
}
|
|
27
|
+
}, 'end query')
|
|
28
|
+
},
|
|
29
|
+
onQueryError: (_query, { text }, err) => {
|
|
30
|
+
log.error({
|
|
31
|
+
query: {
|
|
32
|
+
text,
|
|
33
|
+
error: err.message
|
|
34
|
+
}
|
|
35
|
+
}, 'query error')
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
return db
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function connect ({ connectionString, log, onDatabaseLoad, ignore = {}, autoTimestamp = true, hooks = {} }) {
|
|
42
|
+
// TODO validate config using the schema
|
|
43
|
+
if (!connectionString) {
|
|
44
|
+
throw new Error('connectionString is required')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let queries
|
|
48
|
+
let sql
|
|
49
|
+
let db
|
|
50
|
+
|
|
51
|
+
/* istanbul ignore next */
|
|
52
|
+
if (connectionString.indexOf('postgres') === 0) {
|
|
53
|
+
const createConnectionPoolPg = require('@databases/pg')
|
|
54
|
+
db = await buildConnection(log, createConnectionPoolPg, connectionString)
|
|
55
|
+
sql = createConnectionPoolPg.sql
|
|
56
|
+
queries = queriesFactory.pg
|
|
57
|
+
db.isPg = true
|
|
58
|
+
} else if (connectionString.indexOf('mysql') === 0) {
|
|
59
|
+
const createConnectionPoolMysql = require('@databases/mysql')
|
|
60
|
+
db = await buildConnection(log, createConnectionPoolMysql, connectionString)
|
|
61
|
+
sql = createConnectionPoolMysql.sql
|
|
62
|
+
const version = (await db.query(sql`SELECT VERSION()`))[0]['VERSION()']
|
|
63
|
+
db.version = version
|
|
64
|
+
db.isMariaDB = version.indexOf('maria') !== -1
|
|
65
|
+
if (db.isMariaDB) {
|
|
66
|
+
queries = queriesFactory.mariadb
|
|
67
|
+
} else {
|
|
68
|
+
db.isMySQL = true
|
|
69
|
+
queries = queriesFactory.mysql
|
|
70
|
+
}
|
|
71
|
+
} else if (connectionString.indexOf('sqlite') === 0) {
|
|
72
|
+
const sqlite = require('@databases/sqlite')
|
|
73
|
+
const path = connectionString.replace('sqlite://', '')
|
|
74
|
+
db = sqlite(connectionString === 'sqlite://:memory:' ? undefined : path)
|
|
75
|
+
sql = sqlite.sql
|
|
76
|
+
queries = queriesFactory.sqlite
|
|
77
|
+
db.isSQLite = true
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error('You must specify either postgres, mysql or sqlite as protocols')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const entities = {}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
/* istanbul ignore else */
|
|
86
|
+
if (typeof onDatabaseLoad === 'function') {
|
|
87
|
+
await onDatabaseLoad(db, sql)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tables = await queries.listTables(db, sql)
|
|
91
|
+
|
|
92
|
+
for (const table of tables) {
|
|
93
|
+
// The following line is a safety net when developing this module,
|
|
94
|
+
// it should never happen.
|
|
95
|
+
/* istanbul ignore next */
|
|
96
|
+
if (typeof table !== 'string') {
|
|
97
|
+
throw new Error(`Table must be a string, got '${table}'`)
|
|
98
|
+
}
|
|
99
|
+
if (ignore[table] === true) {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entity = await buildEntity(db, sql, log, table, queries, autoTimestamp, ignore[table] || {})
|
|
104
|
+
// Check for primary key of all entities
|
|
105
|
+
if (!entity.primaryKey) {
|
|
106
|
+
throw new Error(`Cannot find primary key for ${entity.name} entity`)
|
|
107
|
+
}
|
|
108
|
+
entities[entity.singularName] = entity
|
|
109
|
+
if (hooks[entity.name]) {
|
|
110
|
+
addEntityHooks(entity.singularName, hooks[entity.name])
|
|
111
|
+
} else if (hooks[entity.singularName]) {
|
|
112
|
+
addEntityHooks(entity.singularName, hooks[entity.singularName])
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) /* istanbul ignore next */ {
|
|
116
|
+
db.dispose()
|
|
117
|
+
throw err
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
db,
|
|
122
|
+
sql,
|
|
123
|
+
entities,
|
|
124
|
+
addEntityHooks
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function addEntityHooks (entityName, hooks) {
|
|
128
|
+
const entity = entities[entityName]
|
|
129
|
+
if (!entity) {
|
|
130
|
+
throw new Error('Cannot find entity ' + entityName)
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(hooks)) {
|
|
133
|
+
if (hooks[key] && entity[key]) {
|
|
134
|
+
entity[key] = hooks[key].bind(null, entity[key])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function sqlMapper (app, opts) {
|
|
141
|
+
const mapper = await connect({
|
|
142
|
+
log: app.log,
|
|
143
|
+
...opts
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
app.onClose(() => mapper.db.dispose())
|
|
147
|
+
// TODO this would need to be refactored as other plugins
|
|
148
|
+
// would need to use this same namespace
|
|
149
|
+
app.decorate('platformatic', mapper)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = fp(sqlMapper)
|
|
153
|
+
module.exports.connect = connect
|
|
154
|
+
module.exports.plugin = module.exports
|
|
155
|
+
module.exports.utils = require('./lib/utils')
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@platformatic/sql-mapper",
|
|
3
|
+
"version": "0.0.23",
|
|
4
|
+
"description": "A data mapper utility for SQL databases",
|
|
5
|
+
"main": "mapper.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/platformatic/platformatic.git"
|
|
9
|
+
},
|
|
10
|
+
"author": "Matteo Collina <hello@matteocollina.com>",
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/platformatic/platformatic/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/platformatic/platformatic#readme",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"fastify": "^4.5.3",
|
|
18
|
+
"snazzy": "^9.0.0",
|
|
19
|
+
"standard": "^17.0.0",
|
|
20
|
+
"tap": "^16.0.0",
|
|
21
|
+
"tsd": "^0.24.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@databases/mysql": "^5.2.0",
|
|
25
|
+
"@databases/pg": "^5.3.0",
|
|
26
|
+
"@databases/sql": "^3.2.0",
|
|
27
|
+
"@databases/sqlite": "^4.0.0",
|
|
28
|
+
"camelcase": "^6.0.0",
|
|
29
|
+
"fastify-plugin": "^4.1.0",
|
|
30
|
+
"inflected": "^2.1.0"
|
|
31
|
+
},
|
|
32
|
+
"tsd": {
|
|
33
|
+
"directory": "test/types"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "standard | snazzy && npm run test:typescript && npm run test:postgresql && npm run test:mariadb && npm run test:mysql && npm run test:mysql8 && npm run test:sqlite",
|
|
37
|
+
"test:postgresql": "DB=postgresql tap test/*.test.js",
|
|
38
|
+
"test:mariadb": "DB=mariadb tap test/*.test.js",
|
|
39
|
+
"test:mysql": "DB=mysql tap test/*.test.js",
|
|
40
|
+
"test:mysql8": "DB=mysql8 tap test/*.test.js",
|
|
41
|
+
"test:sqlite": "DB=sqlite tap test/*.test.js",
|
|
42
|
+
"test:typescript": "tsd"
|
|
43
|
+
}
|
|
44
|
+
}
|