diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..69909c3d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +**/generated/** +**/dist/** diff --git a/packages/demo/src/index.ts b/packages/demo/src/index.ts index ad894fa5..8cdd465a 100644 --- a/packages/demo/src/index.ts +++ b/packages/demo/src/index.ts @@ -10,39 +10,43 @@ export const getApp = () => { res.status(500).send(`${err}`) } - app.post('/api/messages', (req, res) => slonik - .connect(async conn => { - const content = req.query.content - const id = await conn.oneFirst(sql.MessageId` + app.post('/api/messages', (req, res) => + slonik + .connect(async conn => { + const content = req.query.content + const id = await conn.oneFirst(sql.MessageId` insert into messages(content) values (${content}) returning id `) - res.status(201).send({id}) - }) - .catch(handleError(res)) + res.status(201).send({id}) + }) + .catch(handleError(res)), ) - app.get('/api/messages', (req, res) => slonik - .connect(async conn => { - let {before} = req.query - const messages = await conn.any(sql.Message` + app.get('/api/messages', (req, res) => + slonik + .connect(async conn => { + let {before} = req.query + const messages = await conn.any(sql.Message` select * from messages where id < ${before || 9999999} order by created_at desc limit 10 `) - res.status(200).send(messages.map(m => ({ - id: m.id, - text: m.content, - secondsAgo: Math.floor((Date.now() - m.created_at.getTime()) / 1000), - }))) - }) - .catch(handleError(res)) + res.status(200).send( + messages.map(m => ({ + id: m.id, + text: m.content, + secondsAgo: Math.floor((Date.now() - m.created_at.getTime()) / 1000), + })), + ) + }) + .catch(handleError(res)), ) app.get('/', (_req, res) => res.sendFile(resolve(__dirname + '/../index.html'))) - + return app } diff --git a/packages/demo/test/index.test.ts b/packages/demo/test/index.test.ts index 80052933..a4d5707d 100644 --- a/packages/demo/test/index.test.ts +++ b/packages/demo/test/index.test.ts @@ -2,7 +2,7 @@ process.env.POSTGRES_CONNECTION_STRING = 'postgresql://postgres:postgres@localho import {getApp} from '../src' import * as supertest from 'supertest' -import { slonik, sql } from '../src/db'; +import {slonik, sql} from '../src/db' describe('demo app', () => { const testApp = supertest(getApp()) @@ -11,11 +11,13 @@ describe('demo app', () => { it('gets and posts messages', async () => { const {body: empty} = await testApp.get('/api/messages') - + expect(empty).toEqual([]) - const {body: {id: newMessageId}} = await testApp.post('/api/messages?content=abc') - + const { + body: {id: newMessageId}, + } = await testApp.post('/api/messages?content=abc') + expect(newMessageId).toBeGreaterThanOrEqual(0) const {body: nonEmpty} = await testApp.get('/api/messages') diff --git a/packages/migrator/src/index.ts b/packages/migrator/src/index.ts index d02be952..e57985de 100644 --- a/packages/migrator/src/index.ts +++ b/packages/migrator/src/index.ts @@ -5,8 +5,8 @@ import {map, pick} from 'lodash/fp' import {basename, dirname, join} from 'path' import * as Umzug from 'umzug' import {sql, DatabasePoolType} from 'slonik' -import { raw } from 'slonik-sql-tag-raw' -import { inspect } from 'util'; +import {raw} from 'slonik-sql-tag-raw' +import {inspect} from 'util' export interface SlonikMigratorOptions { slonik: DatabasePoolType @@ -43,18 +43,23 @@ export const setupSlonikMigrator = ({ return _log(...args) }, JSON.stringify) const createMigrationTable = once(async () => { - void await slonik.query(sql` + void (await slonik.query(sql` create table if not exists ${sql.identifier([migrationTableName])}( name text primary key, hash text not null, date timestamptz not null default now() ) - `) + `)) }) - const hash = (migrationName: string) => createHash('md5') - .update(readFileSync(join(migrationsPath, migrationName), 'utf8').trim().replace(/\s+/g, ' ')) - .digest('hex') - .slice(0, 10) + const hash = (migrationName: string) => + createHash('md5') + .update( + readFileSync(join(migrationsPath, migrationName), 'utf8') + .trim() + .replace(/\s+/g, ' '), + ) + .digest('hex') + .slice(0, 10) const umzug = new Umzug({ logging: log, migrations: { @@ -77,36 +82,36 @@ export const setupSlonikMigrator = ({ log('migrations in database:', migrations) return migrations }) - .then(migrations => migrations.map(r => { - const name = r.name as string - /* istanbul ignore if */ - if (r.hash !== hash(name)) { - log( - `warning:`, - `hash in '${migrationTableName}' table didn't match content on disk.`, - `did you try to change a migration file after it had been run?`, - {migration: r.name, dbHash: r.hash, diskHash: hash(name)} - ) - } - return name - })) + .then(migrations => + migrations.map(r => { + const name = r.name as string + /* istanbul ignore if */ + if (r.hash !== hash(name)) { + log( + `warning:`, + `hash in '${migrationTableName}' table didn't match content on disk.`, + `did you try to change a migration file after it had been run?`, + {migration: r.name, dbHash: r.hash, diskHash: hash(name)}, + ) + } + return name + }), + ) }, async logMigration(name: string) { await createMigrationTable() - await slonik - .query(sql` + await slonik.query(sql` insert into ${sql.identifier([migrationTableName])}(name, hash) values (${name}, ${hash(name)}) `) }, async unlogMigration(name: string) { await createMigrationTable() - await slonik - .query(sql` + await slonik.query(sql` delete from ${sql.identifier([migrationTableName])} where name = ${name} `) - } + }, }, }) @@ -114,7 +119,10 @@ export const setupSlonikMigrator = ({ up: (name?: string) => umzug.up(name).then(map(pick(['file', 'path']))), down: (name?: string) => umzug.down(name).then(map(pick(['file', 'path']))), create: (name: string) => { - const timestamp = new Date().toISOString().replace(/\W/g, '-').replace(/-\d\d-\d\d\dZ/, '') + const timestamp = new Date() + .toISOString() + .replace(/\W/g, '-') + .replace(/-\d\d-\d\d\dZ/, '') const sqlFileName = `${timestamp}.${name}.sql` const downDir = join(migrationsPath, 'down') mkdirSync(downDir, {recursive: true}) @@ -127,7 +135,13 @@ export const setupSlonikMigrator = ({ const [command, name] = process.argv.slice(2) command in migrator ? (migrator as any)[command](name) - : console.warn('command not found. ' + inspect({'commands available': Object.keys(migrator), 'command from cli args': command}, {breakLength: Infinity})) + : console.warn( + 'command not found. ' + + inspect( + {'commands available': Object.keys(migrator), 'command from cli args': command}, + {breakLength: Infinity}, + ), + ) } return migrator diff --git a/packages/typegen/src/index.ts b/packages/typegen/src/index.ts index 62eb0599..3b17ef1a 100644 --- a/packages/typegen/src/index.ts +++ b/packages/typegen/src/index.ts @@ -1,20 +1,27 @@ -import {QueryResultRowType, sql as slonikSql, TaggedTemplateLiteralInvocationType, ValueExpressionType, ClientConfigurationType, DatabasePoolType, createPool} from 'slonik' +import { + QueryResultRowType, + sql as slonikSql, + TaggedTemplateLiteralInvocationType, + ValueExpressionType, + ClientConfigurationType, + DatabasePoolType, + createPool, +} from 'slonik' import * as fs from 'fs' -import { basename, join } from 'path' -import { inspect } from 'util'; -import {EOL} from 'os'; +import {basename, join} from 'path' +import {inspect} from 'util' +import {EOL} from 'os' const keys = (obj: T) => Object.keys(obj) as Array -const fromPairs = (pairs: Array<[K, V]>) => pairs.reduce( - (obj, [k, v]) => ({ ...obj, [k as any]: v }), - {} as Record -) as Record -const orderBy = (list: T[], cb: (value: T) => string | number) => [...list].sort((a, b) => { - const left = cb(a) - const right = cb(b) - return left < right ? -1 : left > right ? 1 : 0 -}) +const fromPairs = (pairs: Array<[K, V]>) => + pairs.reduce((obj, [k, v]) => ({...obj, [k as any]: v}), {} as Record) as Record +const orderBy = (list: T[], cb: (value: T) => string | number) => + [...list].sort((a, b) => { + const left = cb(a) + const right = cb(b) + return left < right ? -1 : left > right ? 1 : 0 + }) export interface GenericSqlTaggedTemplateType { (template: TemplateStringsArray, ...vals: ValueExpressionType[]): TaggedTemplateLiteralInvocationType @@ -50,17 +57,19 @@ export type DefaultType = { export type TypeGenClientConfig = Pick export interface TypeGen { poolConfig: TypeGenClientConfig - sql: typeof slonikSql & { - [K in keyof KnownTypes]: GenericSqlTaggedTemplateType - } & { - [K in string]: GenericSqlTaggedTemplateType> - } + sql: typeof slonikSql & + { + [K in keyof KnownTypes]: GenericSqlTaggedTemplateType + } & + { + [K in string]: GenericSqlTaggedTemplateType> + } } export const setupTypeGen = (config: TypeGenConfig): TypeGen => { const {sql: sqlGetter, poolConfig} = setupSqlGetter(config) const _sql: any = (...args: Parameters) => slonikSql(...args) - Object.keys(config.knownTypes).forEach(name => _sql[name] = sqlGetter(name)) + Object.keys(config.knownTypes).forEach(name => (_sql[name] = sqlGetter(name))) return { poolConfig, sql: new Proxy(_sql, { @@ -69,7 +78,7 @@ export const setupTypeGen = (config: TypeGenConfig): Typ return (slonikSql as any)[key] } if (typeof key === 'string' && !(key in _sql)) { - return _sql[key] = sqlGetter(key) + return (_sql[key] = sqlGetter(key)) } return _sql[key] }, @@ -79,8 +88,9 @@ export const setupTypeGen = (config: TypeGenConfig): Typ export interface TypeGenWithSqlGetter { poolConfig: TypeGenClientConfig - sql: (identifier: Identifier) => - GenericSqlTaggedTemplateType + sql: ( + identifier: Identifier, + ) => GenericSqlTaggedTemplateType } export const createCodegenDirectory = (directory: string) => { @@ -90,8 +100,7 @@ export const createCodegenDirectory = (directory: string) => { export const resetCodegenDirectory = (directory: string) => { if (fs.existsSync(directory)) { - fs.readdirSync(directory) - .forEach(filename => fs.unlinkSync(join(directory, filename))) + fs.readdirSync(directory).forEach(filename => fs.unlinkSync(join(directory, filename))) fs.rmdirSync(directory) } createCodegenDirectory(directory) @@ -103,45 +112,43 @@ export const setupSqlGetter = (config: TypeGenConfig): T } const typeParsers = config.typeMapper ? keys(config.typeMapper).map(name => ({ - name: name as string, - parse: config.typeMapper![name]![1], - })) + name: name as string, + parse: config.typeMapper![name]![1], + })) : [] if (!config.writeTypes) { // not writing types, no need to track queries or intercept results return { - sql: Object.assign( - () => slonikSql, - fromPairs(keys(config.knownTypes).map(k => [k, slonikSql])), - ), + sql: Object.assign(() => slonikSql, fromPairs(keys(config.knownTypes).map(k => [k, slonikSql]))), poolConfig: { interceptors: [], typeParsers, - } + }, } } const writeTypes = getFsTypeWriter(config.writeTypes) - + let _oidToTypeName: undefined | Record = undefined - const mapping: Record = config.typeMapper || {} as any + const mapping: Record = config.typeMapper || ({} as any) const typescriptTypeName = (dataTypeId: number): string => { const typeName = _oidToTypeName && _oidToTypeName[dataTypeId] - const typescriptTypeName = typeName && (() => { - const [customType] = mapping[typeName] || [undefined] - return customType || builtInTypeMappings[typeName] - })() + const typescriptTypeName = + typeName && + (() => { + const [customType] = mapping[typeName] || [undefined] + return customType || builtInTypeMappings[typeName] + })() return typescriptTypeName || 'unknown' } const _map: Record = {} - const mapKey = (sqlValue: { sql: string, values?: any }) => - JSON.stringify([sqlValue.sql, sqlValue.values]) + const mapKey = (sqlValue: {sql: string; values?: any}) => JSON.stringify([sqlValue.sql, sqlValue.values]) const sql: TypeGenWithSqlGetter['sql'] = identifier => { const _wrappedSqlFunction = (...args: Parameters) => { const result = slonikSql(...args) const key = mapKey(result) - const _identifiers = _map[key] = _map[key] || [] + const _identifiers = (_map[key] = _map[key] || []) _identifiers.push(identifier) return result } @@ -150,67 +157,81 @@ export const setupSqlGetter = (config: TypeGenConfig): T return { sql, poolConfig: { - interceptors: [{ - afterPoolConnection: async (_context, connection) => { - if (!_oidToTypeName && typeof config.writeTypes === 'string') { - const types = orderBy( - await connection.any(slonikSql` + interceptors: [ + { + afterPoolConnection: async (_context, connection) => { + if (!_oidToTypeName && typeof config.writeTypes === 'string') { + const types = orderBy( + await connection.any(slonikSql` select typname, oid from pg_type where (typnamespace = 11 and typname not like 'pg_%') or (typrelid = 0 and typelem = 0) `), - t => `${t.typname}`.replace(/^_/, 'zzz') - ) - _oidToTypeName = fromPairs(types.map(t => [t.oid as number, t.typname as string])) - fs.writeFileSync( - join(config.writeTypes, '_pg_types.ts'), - [ - `${header}`, - `export const _pg_types = ${inspect(fromPairs(types.map(t => [t.typname, t.typname])))} as const`, - `export type _pg_types = typeof _pg_types${EOL}`, - ].join(EOL + EOL), - ) - } - return null - }, - afterQueryExecution: async ({ originalQuery }, _query, result) => { - const trimmedSql = originalQuery.sql.replace(/^\r?\n+/, '').trimRight() - const _identifiers = _map[mapKey(originalQuery)] - _identifiers && _identifiers.forEach(identifier => writeTypes( - identifier, - result.fields.map(f => ({ - name: f.name, - value: typescriptTypeName(f.dataTypeId), - description: _oidToTypeName && `pg_type.typname: ${_oidToTypeName[f.dataTypeId]}`, - })), - trimmedSql.trim(), - )) + t => `${t.typname}`.replace(/^_/, 'zzz'), + ) + _oidToTypeName = fromPairs(types.map(t => [t.oid as number, t.typname as string])) + fs.writeFileSync( + join(config.writeTypes, '_pg_types.ts'), + [ + `${header}`, + `export const _pg_types = ${inspect(fromPairs(types.map(t => [t.typname, t.typname])))} as const`, + `export type _pg_types = typeof _pg_types${EOL}`, + ].join(EOL + EOL), + ) + } + return null + }, + afterQueryExecution: async ({originalQuery}, _query, result) => { + const trimmedSql = originalQuery.sql.replace(/^\r?\n+/, '').trimRight() + const _identifiers = _map[mapKey(originalQuery)] + _identifiers && + _identifiers.forEach(identifier => + writeTypes( + identifier, + result.fields.map(f => ({ + name: f.name, + value: typescriptTypeName(f.dataTypeId), + description: _oidToTypeName && `pg_type.typname: ${_oidToTypeName[f.dataTypeId]}`, + })), + trimmedSql.trim(), + ), + ) - // todo: fix types and remove this stupid cast? @types/slonik seems to expect null here - return result as any as null - } - }], + // todo: fix types and remove this stupid cast? @types/slonik seems to expect null here + return (result as any) as null + }, + }, + ], typeParsers, - } + }, } } -export interface Property { name: string, value: string, description?: string } +export interface Property { + name: string + value: string + description?: string +} const blockComment = (str?: string) => str && '/** ' + str.replace(/\*\//g, '') + ' */' const codegen = { writeInterface: (name: string, properties: Property[], description?: string) => `export interface ${name} ` + codegen.writeInterfaceBody(properties, description), - writeInterfaceBody: (properties: Property[], description?: string) => [ - blockComment(description), - `{`, - ...properties.map(p => [ - blockComment(p.description), - `${p.name}: ${p.value}` - ].filter(Boolean).map(s => ' ' + s).join(EOL)), - `}`, - ].filter(Boolean).join(EOL) + writeInterfaceBody: (properties: Property[], description?: string) => + [ + blockComment(description), + `{`, + ...properties.map(p => + [blockComment(p.description), `${p.name}: ${p.value}`] + .filter(Boolean) + .map(s => ' ' + s) + .join(EOL), + ), + `}`, + ] + .filter(Boolean) + .join(EOL), } const header = [ @@ -219,68 +240,69 @@ const header = [ `// this file is generated by a tool; don't change it manually.`, ].join(EOL) -const getFsTypeWriter = (generatedPath: string) => - (typeName: string, properties: Property[], description: string) => { - const tsPath = join(generatedPath, `${typeName}.ts`) - const existingContent = fs.existsSync(tsPath) - ? fs.readFileSync(tsPath, 'utf8') - : '' - const metaDeclaration = `export const ${typeName}_meta_v0 = ` - const lines = existingContent.split(EOL).map(line => line.trim()) - const metaLine = lines.find(line => line.startsWith(metaDeclaration)) || '[]' - let _entries: Array = JSON.parse(metaLine.replace(metaDeclaration, '')) +const getFsTypeWriter = (generatedPath: string) => (typeName: string, properties: Property[], description: string) => { + const tsPath = join(generatedPath, `${typeName}.ts`) + const existingContent = fs.existsSync(tsPath) ? fs.readFileSync(tsPath, 'utf8') : '' + const metaDeclaration = `export const ${typeName}_meta_v0 = ` + const lines = existingContent.split(EOL).map(line => line.trim()) + const metaLine = lines.find(line => line.startsWith(metaDeclaration)) || '[]' + let _entries: Array = JSON.parse(metaLine.replace(metaDeclaration, '')) - const newEntry = { properties, description } - _entries.unshift(newEntry) - _entries = orderBy(_entries, e => e.description) - _entries = _entries - .filter((e, i, arr) => i === arr.findIndex(x => x.description === e.description)) + const newEntry = {properties, description} + _entries.unshift(newEntry) + _entries = orderBy(_entries, e => e.description) + _entries = _entries.filter((e, i, arr) => i === arr.findIndex(x => x.description === e.description)) - const contnt = [ - header, - ``, - `export interface ${typeName}_QueryTypeMap {`, - ' ' + _entries + const contnt = [ + header, + ``, + `export interface ${typeName}_QueryTypeMap {`, + ' ' + + _entries .map(e => `[${JSON.stringify(e.description)}]: ${codegen.writeInterfaceBody(e.properties)}`) .join(EOL) .replace(/\r?\n/g, EOL + ' '), - `}`, - ``, - `export type ${typeName}_UnionType = ${typeName}_QueryTypeMap[keyof ${typeName}_QueryTypeMap]`, - ``, - `export type ${typeName} = {`, - ` [K in keyof ${typeName}_UnionType]: ${typeName}_UnionType[K]`, - `}`, - `export const ${typeName} = {} as ${typeName}`, - ``, - `${metaDeclaration}${JSON.stringify(_entries)}`, - ``, - ].join(EOL) + `}`, + ``, + `export type ${typeName}_UnionType = ${typeName}_QueryTypeMap[keyof ${typeName}_QueryTypeMap]`, + ``, + `export type ${typeName} = {`, + ` [K in keyof ${typeName}_UnionType]: ${typeName}_UnionType[K]`, + `}`, + `export const ${typeName} = {} as ${typeName}`, + ``, + `${metaDeclaration}${JSON.stringify(_entries)}`, + ``, + ].join(EOL) - void fs.writeFileSync(tsPath, contnt, 'utf8') + void fs.writeFileSync(tsPath, contnt, 'utf8') - const knownTypes = fs.readdirSync(generatedPath) - .filter(filename => filename !== 'index.ts') - .map(filename => basename(filename, '.ts')) + const knownTypes = fs + .readdirSync(generatedPath) + .filter(filename => filename !== 'index.ts') + .map(filename => basename(filename, '.ts')) - void fs.writeFileSync( - join(generatedPath, `index.ts`), - [ - header, - ...knownTypes.map(name => `import {${name}} from './${name}'`), - '', - ...knownTypes.map(name => `export {${name}}`), - '', - codegen.writeInterface('KnownTypes', knownTypes.map(name => ({ name, value: name }))), - '', - '/** runtime-accessible object with phantom type information of query results. */', - `export const knownTypes: KnownTypes = {`, - ...knownTypes.map(name => ` ${name},`), - `}`, - '', - ].join(EOL) - ) - } + void fs.writeFileSync( + join(generatedPath, `index.ts`), + [ + header, + ...knownTypes.map(name => `import {${name}} from './${name}'`), + '', + ...knownTypes.map(name => `export {${name}}`), + '', + codegen.writeInterface( + 'KnownTypes', + knownTypes.map(name => ({name, value: name})), + ), + '', + '/** runtime-accessible object with phantom type information of query results. */', + `export const knownTypes: KnownTypes = {`, + ...knownTypes.map(name => ` ${name},`), + `}`, + '', + ].join(EOL), + ) +} const builtInTypeMappings: Record = { text: 'string',