@platformatic/sql-mapper 0.22.0 → 0.23.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 CHANGED
@@ -14,7 +14,7 @@ function lowerCaseFirst (str) {
14
14
  return str.charAt(0).toLowerCase() + str.slice(1)
15
15
  }
16
16
 
17
- function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relations, queries, autoTimestamp, schema, useSchemaInName, limitConfig) {
17
+ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relations, queries, autoTimestamp, schema, useSchemaInName, limitConfig, columns, constraintsList) {
18
18
  /* istanbul ignore next */ // Ignoring because this won't be fully covered by DB not supporting schemas (SQLite)
19
19
  const entityName = useSchemaInName ? toUpperFirst(`${schema}${toSingular(table)}`) : toSingular(table)
20
20
  /* istanbul ignore next */
@@ -339,9 +339,9 @@ function createMapper (defaultDb, sql, log, table, fields, primaryKeys, relation
339
339
  }
340
340
  }
341
341
 
342
- async function buildEntity (db, sql, log, table, queries, autoTimestamp, schema, useSchemaInName, ignore, limitConfig, schemaList) {
342
+ function buildEntity (db, sql, log, table, queries, autoTimestamp, schema, useSchemaInName, ignore, limitConfig, schemaList, columns, constraintsList) {
343
343
  // Compute the columns
344
- const columns = (await queries.listColumns(db, sql, table, schema)).filter((c) => !ignore[c.column_name])
344
+ columns = columns.filter((c) => !ignore[c.column_name])
345
345
  const fields = columns.reduce((acc, column) => {
346
346
  acc[column.column_name] = {
347
347
  sqlType: column.udt_name,
@@ -368,33 +368,26 @@ async function buildEntity (db, sql, log, table, queries, autoTimestamp, schema,
368
368
  acc[column.column_name].isGenerated = column.is_generated.includes('GENERATED')
369
369
  }
370
370
 
371
+ // To get enum values in pg
372
+ /* istanbul ignore next */
373
+ if (column.enum) {
374
+ acc[column.column_name].enum = column.enum
375
+ }
376
+
371
377
  return acc
372
378
  }, {})
373
379
 
374
- // To get enum values in pg
375
- /* istanbul ignore next */
376
- if (db.isPg) {
377
- const enums = await queries.listEnumValues(db, sql, table, schema)
378
- for (const enumValue of enums) {
379
- if (!fields[enumValue.column_name].enum) {
380
- fields[enumValue.column_name].enum = [enumValue.enumlabel]
381
- } else {
382
- fields[enumValue.column_name].enum.push(enumValue.enumlabel)
383
- }
384
- }
385
- }
386
380
  const currentRelations = []
387
381
 
388
- const constraintsList = await queries.listConstraints(db, sql, table, schema)
389
382
  const primaryKeys = new Set()
390
383
 
391
384
  /* istanbul ignore next */
392
385
  function checkSQLitePrimaryKey (constraint) {
393
386
  if (db.isSQLite) {
394
- const validTypes = ['integer', 'uuid', 'serial']
387
+ const validTypes = ['varchar', 'integer', 'uuid', 'serial']
395
388
  const pkType = fields[constraint.column_name].sqlType.toLowerCase()
396
389
  if (!validTypes.includes(pkType)) {
397
- throw new Error(`Invalid Primary Key type. Expected "integer", found "${pkType}"`)
390
+ throw new Error(`Invalid Primary Key type: "${pkType}". We support the following: ${validTypes.join(', ')}.`)
398
391
  }
399
392
  }
400
393
  }
package/mapper.js CHANGED
@@ -46,7 +46,7 @@ const defaultAutoTimestampFields = {
46
46
  updatedAt: 'updated_at'
47
47
  }
48
48
 
49
- async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10, ignore = {}, autoTimestamp = true, hooks = {}, schema, limit = {} }) {
49
+ async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10, ignore = {}, autoTimestamp = true, hooks = {}, schema, limit = {}, dbschema }) {
50
50
  if (typeof autoTimestamp === 'boolean' && autoTimestamp === true) {
51
51
  autoTimestamp = defaultAutoTimestampFields
52
52
  }
@@ -110,9 +110,33 @@ async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10,
110
110
  await onDatabaseLoad(db, sql)
111
111
  }
112
112
 
113
- const tablesWithSchema = await queries.listTables(db, sql, schemaList)
113
+ if (!dbschema) {
114
+ dbschema = await queries.listTables(db, sql, schemaList)
115
+
116
+ // TODO make this parallel or a single query
117
+ for (const wrap of dbschema) {
118
+ const { table, schema } = wrap
119
+ const columns = await queries.listColumns(db, sql, table, schema)
120
+ wrap.constraints = await queries.listConstraints(db, sql, table, schema)
121
+ wrap.columns = columns
122
+
123
+ // To get enum values in pg
124
+ /* istanbul ignore next */
125
+ if (db.isPg) {
126
+ const enums = await queries.listEnumValues(db, sql, table, schema)
127
+ for (const enumValue of enums) {
128
+ const column = columns.find(column => column.column_name === enumValue.column_name)
129
+ if (!column.enum) {
130
+ column.enum = [enumValue.enumlabel]
131
+ } else {
132
+ column.enum.push(enumValue.enumlabel)
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
114
138
 
115
- for (const { table, schema } of tablesWithSchema) {
139
+ for (const { table, schema, columns, constraints } of dbschema) {
116
140
  // The following line is a safety net when developing this module,
117
141
  // it should never happen.
118
142
  /* istanbul ignore next */
@@ -122,7 +146,7 @@ async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10,
122
146
  if (ignore[table] === true) {
123
147
  continue
124
148
  }
125
- const entity = await buildEntity(db, sql, log, table, queries, autoTimestamp, schema, useSchema, ignore[table] || {}, limit, schemaList)
149
+ const entity = buildEntity(db, sql, log, table, queries, autoTimestamp, schema, useSchema, ignore[table] || {}, limit, schemaList, columns, constraints)
126
150
  // Check for primary key of all entities
127
151
  if (entity.primaryKeys.size === 0) {
128
152
  log.warn({ table }, 'Cannot find any primary keys for table')
@@ -137,18 +161,19 @@ async function connect ({ connectionString, log, onDatabaseLoad, poolSize = 10,
137
161
  addEntityHooks(entity.singularName, hooks[entity.singularName])
138
162
  }
139
163
  }
164
+
165
+ return {
166
+ db,
167
+ sql,
168
+ entities,
169
+ addEntityHooks,
170
+ dbschema
171
+ }
140
172
  } catch (err) /* istanbul ignore next */ {
141
173
  db.dispose()
142
174
  throw err
143
175
  }
144
176
 
145
- return {
146
- db,
147
- sql,
148
- entities,
149
- addEntityHooks
150
- }
151
-
152
177
  function addEntityHooks (entityName, hooks) {
153
178
  const entity = entities[entityName]
154
179
  if (!entity) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/sql-mapper",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "A data mapper utility for SQL databases",
5
5
  "main": "mapper.js",
6
6
  "repository": {
@@ -346,27 +346,32 @@ test('[SQLite] - UUID', { skip: !isSQLite }, async ({ pass, teardown, same, equa
346
346
  }
347
347
  })
348
348
 
349
- test('[SQLite] throws if PK is not INTEGER', { skip: !isSQLite }, async ({ fail, equal, teardown, rejects }) => {
349
+ test('[SQLite] allows to have VARCHAR PK', { skip: !isSQLite }, async ({ same, teardown }) => {
350
350
  async function onDatabaseLoad (db, sql) {
351
351
  await clear(db, sql)
352
+ teardown(() => db.dispose())
353
+
352
354
  await db.query(sql`CREATE TABLE pages (
353
- id int PRIMARY KEY,
354
- title varchar(255) NOT NULL,
355
- content text NOT NULL
355
+ id varchar(255) PRIMARY KEY,
356
+ title varchar(255) NOT NULL
356
357
  );`)
357
358
  }
358
- try {
359
- await connect({
360
- connectionString: connInfo.connectionString,
361
- log: fakeLogger,
362
- onDatabaseLoad,
363
- ignore: {},
364
- hooks: {}
365
- })
366
- fail()
367
- } catch (err) {
368
- equal(err.message, 'Invalid Primary Key type. Expected "integer", found "int"')
369
- }
359
+ const mapper = await connect({
360
+ connectionString: connInfo.connectionString,
361
+ log: fakeLogger,
362
+ onDatabaseLoad,
363
+ ignore: {},
364
+ hooks: {}
365
+ })
366
+ const pageEntity = mapper.entities.page
367
+ const [newPage] = await pageEntity.insert({
368
+ fields: ['id', 'title'],
369
+ inputs: [{ id: 'varchar_id', title: '13th page with explicit id equal to 13' }]
370
+ })
371
+ same(newPage, {
372
+ id: 'varchar_id',
373
+ title: '13th page with explicit id equal to 13'
374
+ })
370
375
  })
371
376
 
372
377
  test('mixing snake and camel case', async ({ pass, teardown, same, equal }) => {
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test } = require('tap')
4
4
 
5
- const { clear, connInfo, isMysql8 } = require('./helper')
5
+ const { clear, connInfo, isMysql8, isSQLite } = require('./helper')
6
6
  const { connect } = require('..')
7
7
  const fakeLogger = {
8
8
  trace: () => {},
@@ -36,7 +36,7 @@ test('unique key', async ({ equal, not, same, teardown }) => {
36
36
  equal(pageEntity.name, 'Page')
37
37
  equal(pageEntity.singularName, 'page')
38
38
  equal(pageEntity.pluralName, 'pages')
39
- if (isMysql8) {
39
+ if (isMysql8 || isSQLite) {
40
40
  same(pageEntity.primaryKeys, new Set(['name']))
41
41
  equal(pageEntity.camelCasedFields.name.primaryKey, true)
42
42
  } else {
@@ -61,7 +61,7 @@ test('uses tables from different schemas', { skip: isSQLite }, async ({ pass, te
61
61
  pass()
62
62
  })
63
63
 
64
- test('find enums correctly using schemas', { skip: isSQLite }, async ({ pass, teardown, equal }) => {
64
+ test('find enums correctly using schemas', { skip: isSQLite }, async ({ pass, teardown, equal, match }) => {
65
65
  async function onDatabaseLoad (db, sql) {
66
66
  await clear(db, sql)
67
67
  teardown(() => db.dispose())
@@ -102,6 +102,31 @@ test('find enums correctly using schemas', { skip: isSQLite }, async ({ pass, te
102
102
  equal(pageEntity.name, 'Test1Page')
103
103
  equal(pageEntity.singularName, 'test1Page')
104
104
  equal(pageEntity.pluralName, 'test1Pages')
105
+ match(mapper.dbschema, [
106
+ {
107
+ schema: 'test1',
108
+ table: 'pages',
109
+ constraints: [
110
+ {
111
+ constraint_type: isMysql8 ? 'UNIQUE' : 'PRIMARY KEY'
112
+ }
113
+ ],
114
+ columns: [
115
+ {
116
+ column_name: 'id',
117
+ is_nullable: 'NO'
118
+ },
119
+ {
120
+ column_name: 'title',
121
+ is_nullable: 'NO'
122
+ },
123
+ {
124
+ column_name: 'type',
125
+ is_nullable: 'YES'
126
+ }
127
+ ]
128
+ }
129
+ ])
105
130
  pass()
106
131
  })
107
132
 
@@ -379,3 +404,71 @@ test('uses tables from different schemas with FK', { skip: isSQLite }, async ({
379
404
  equal(userRelation.entityName, 'test2User')
380
405
  pass()
381
406
  })
407
+
408
+ test('recreate mapper from schema', async ({ pass, teardown, equal, match, fail }) => {
409
+ async function onDatabaseLoad (db, sql) {
410
+ await clear(db, sql)
411
+ teardown(() => db.dispose())
412
+
413
+ if (isMysql || isMysql8) {
414
+ await db.query(sql`
415
+ CREATE TABLE IF NOT EXISTS \`pages\` (
416
+ id SERIAL PRIMARY KEY,
417
+ title VARCHAR(255) NOT NULL
418
+ );`)
419
+ } else if (isPg) {
420
+ await db.query(sql`
421
+ CREATE TABLE IF NOT EXISTS "pages" (
422
+ id SERIAL PRIMARY KEY,
423
+ title VARCHAR(255) NOT NULL
424
+ );`)
425
+ } else if (isSQLite) {
426
+ await db.query(sql`
427
+ CREATE TABLE IF NOT EXISTS "pages" (
428
+ id INTEGER PRIMARY KEY,
429
+ title VARCHAR(255) NOT NULL
430
+ );`)
431
+ } else {
432
+ await db.query(sql`CREATE TABLE IF NOT EXISTS "pages" (
433
+ id SERIAL PRIMARY KEY,
434
+ title VARCHAR(255) NOT NULL,
435
+ );`)
436
+ }
437
+ }
438
+ const mapper = await connect({
439
+ connectionString: connInfo.connectionString,
440
+ log: fakeLogger,
441
+ onDatabaseLoad,
442
+ ignore: {},
443
+ hooks: {}
444
+ })
445
+ const dbschema = mapper.dbschema
446
+ const knownQueries = [
447
+ 'SELECT VERSION()'
448
+ ]
449
+ const mapper2 = await connect({
450
+ connectionString: connInfo.connectionString,
451
+ log: {
452
+ trace (msg) {
453
+ if (knownQueries.indexOf(msg.query?.text) < 0) {
454
+ console.log(msg)
455
+ fail('no trace')
456
+ }
457
+ },
458
+ error (...msg) {
459
+ console.log(...msg)
460
+ fail('no error')
461
+ }
462
+ },
463
+ dbschema,
464
+ ignore: {},
465
+ hooks: {}
466
+ })
467
+ teardown(() => mapper2.db.dispose())
468
+
469
+ const pageEntity = mapper2.entities.page
470
+ equal(pageEntity.name, 'Page')
471
+ equal(pageEntity.singularName, 'page')
472
+ equal(pageEntity.pluralName, 'pages')
473
+ pass()
474
+ })