Skip to content

Commit 89cabf1

Browse files
authored
Add prettyLogger to migrator (#412)
* Add prettyLogger to migrator Add exported prettyLogger object which converts log objects to strings and outputs them to console. Update incorrect default logger value. * Handle invalid input * Narrow MESSAGE_FORMATS type * Move constants outside of log function * Move prettyLogger to static property of SlonikMigrator * Add tests for prettyLogger * Add event name to up and down events
1 parent 54c1ec8 commit 89cabf1

File tree

4 files changed

+135
-3
lines changed

4 files changed

+135
-3
lines changed

packages/demo/migrate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const migrator = new SlonikMigrator({
55
slonik,
66
migrationTableName: 'demo_migration',
77
migrationsPath: __dirname + '/migrations',
8-
logger: console,
8+
logger: SlonikMigrator.prettyLogger,
99
})
1010

1111
migrator.runAsCLI()

packages/migrator/readme.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const migrator = new SlonikMigrator({
5656
migrationsPath: __dirname + '/migrations',
5757
migrationTableName: 'migration',
5858
slonik,
59+
logger: SlonikMigrator.prettyLogger,
5960
})
6061

6162
migrator.runAsCLI()
@@ -364,8 +365,10 @@ parameters for the `SlonikMigrator` constructor
364365
| `slonik` | slonik database pool instance, created by `createPool`. | N/A |
365366
| `migrationsPath` | path pointing to directory on filesystem where migration files will live. | N/A |
366367
| `migrationTableName` | the name for the table migrations information will be stored in. You can change this to avoid a clash with existing tables, or to conform with your team's naming standards. Set to an array to change the schema e.g. `['public', 'dbmigrations']` | N/A |
367-
| `logger` | how information about the migrations will be logged. You can set to `undefined` to prevent logs appearing at all. | `console` |
368+
| `logger` | how information about the migrations will be logged. You can set to `console` to log raw objects to console, `undefined` to prevent logs appearing at all, use `SlonikMigrator.prettyLogger` or supply a custom logger. | `undefined` |
369+
370+
`SlonikMigrator.prettyLogger` logs all messages to console. Known events are prettified to strings, unknown events or unexpected message properties in known events are logged as objects.
368371

369372
## Implementation
370373

371-
Under the hood, the library thinly wraps [umzug](https://npmjs.com/package/umzug) with a custom slonik-based stoage implementation.
374+
Under the hood, the library thinly wraps [umzug](https://npmjs.com/package/umzug) with a custom slonik-based storage implementation.

packages/migrator/src/index.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ export class SlonikMigrator extends umzug.Umzug<SlonikMigratorContext> {
5656
}
5757
}
5858

59+
/**
60+
* Logs messages to console. Known events are prettified to strings, unknown
61+
* events or unexpected message properties in known events are logged as objects.
62+
*/
63+
static prettyLogger: NonNullable<SlonikMigratorOptions['logger']> = {
64+
info: message => prettifyAndLog('info', message),
65+
warn: message => prettifyAndLog('warn', message),
66+
error: message => prettifyAndLog('error', message),
67+
debug: message => prettifyAndLog('debug', message),
68+
}
69+
5970
getCli(options?: umzug.CommandLineParserOptions) {
6071
const cli = super.getCli({toolDescription: `@slonik/migrator - PostgreSQL migration tool`, ...options})
6172
cli.addAction(new RepairAction(this))
@@ -403,3 +414,53 @@ class RepairAction extends CommandLineAction {
403414
export interface RepairOptions {
404415
dryRun?: boolean
405416
}
417+
418+
type LogMessage = Record<string, unknown>
419+
420+
const createMessageFormats = <T extends Record<string, (msg: LogMessage) => [string, LogMessage]>>(formats: T) =>
421+
formats
422+
423+
const MESSAGE_FORMATS = createMessageFormats({
424+
created: msg => {
425+
const {event, path, ...rest} = msg
426+
return [`created ${path}`, rest]
427+
},
428+
migrating: msg => {
429+
const {event, name, ...rest} = msg
430+
return [`migrating ${name}`, rest]
431+
},
432+
migrated: msg => {
433+
const {event, name, durationSeconds, ...rest} = msg
434+
return [`migrated ${name} in ${durationSeconds} s`, rest]
435+
},
436+
reverting: msg => {
437+
const {event, name, ...rest} = msg
438+
return [`reverting ${name}`, rest]
439+
},
440+
reverted: msg => {
441+
const {event, name, durationSeconds, ...rest} = msg
442+
return [`reverted ${name} in ${durationSeconds} s`, rest]
443+
},
444+
up: msg => {
445+
const {event, message, ...rest} = msg
446+
return [`up migration completed, ${message}`, rest]
447+
},
448+
down: msg => {
449+
const {event, message, ...rest} = msg
450+
return [`down migration completed, ${message}`, rest]
451+
},
452+
})
453+
454+
function isProperEvent(event: unknown): event is keyof typeof MESSAGE_FORMATS {
455+
return typeof event === 'string' && event in MESSAGE_FORMATS
456+
}
457+
458+
function prettifyAndLog(level: keyof typeof SlonikMigrator.prettyLogger, message: LogMessage) {
459+
const {event} = message || {}
460+
if (!isProperEvent(event)) return console[level](message)
461+
462+
const [messageStr, rest] = MESSAGE_FORMATS[event](message)
463+
console[level](messageStr)
464+
465+
if (Object.keys(rest).length > 0) console[level](rest)
466+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {SlonikMigrator} from '../src'
2+
3+
const {prettyLogger} = SlonikMigrator
4+
5+
describe('prettyLogger', () => {
6+
const originalInfo = console.info
7+
const originalDebug = console.debug
8+
const originalWarn = console.warn
9+
const originalError = console.error
10+
11+
beforeEach(() => {
12+
console.info = jest.fn()
13+
console.debug = jest.fn()
14+
console.warn = jest.fn()
15+
console.error = jest.fn()
16+
})
17+
18+
afterEach(() => {
19+
console.info = originalInfo
20+
console.debug = originalDebug
21+
console.warn = originalWarn
22+
console.error = originalError
23+
})
24+
25+
test('known events', () => {
26+
prettyLogger.info({event: 'created', path: './db/migrations/2022.08.25T12.51.47.test.sql'})
27+
prettyLogger.info({event: 'migrating', name: '2022.08.25T12.51.47.test.sql'})
28+
prettyLogger.debug({event: 'migrated', name: '2022.08.25T12.51.47.test.sql', durationSeconds: 0.024})
29+
prettyLogger.debug({event: 'up', message: 'applied 1 migrations.'})
30+
prettyLogger.warn({event: 'reverting', name: '2022.08.25T12.51.47.test.sql'})
31+
prettyLogger.warn({event: 'reverted', name: '2022.08.25T12.51.47.test.sql', durationSeconds: 0.026})
32+
prettyLogger.error({event: 'down', message: 'reverted 1 migrations.'})
33+
34+
expect((console.info as jest.Mock).mock.calls[0][0]).toBe('created ./db/migrations/2022.08.25T12.51.47.test.sql')
35+
expect((console.info as jest.Mock).mock.calls[1][0]).toBe('migrating 2022.08.25T12.51.47.test.sql')
36+
expect((console.debug as jest.Mock).mock.calls[0][0]).toBe('migrated 2022.08.25T12.51.47.test.sql in 0.024 s')
37+
expect((console.debug as jest.Mock).mock.calls[1][0]).toBe('up migration completed, applied 1 migrations.')
38+
expect((console.warn as jest.Mock).mock.calls[0][0]).toBe('reverting 2022.08.25T12.51.47.test.sql')
39+
expect((console.warn as jest.Mock).mock.calls[1][0]).toBe('reverted 2022.08.25T12.51.47.test.sql in 0.026 s')
40+
expect((console.error as jest.Mock).mock.calls[0][0]).toBe('down migration completed, reverted 1 migrations.')
41+
})
42+
43+
test('known events with additional parameters', () => {
44+
prettyLogger.info({event: 'up', message: 'applied 1 migrations.', totalMigrations: 10})
45+
46+
expect((console.info as jest.Mock).mock.calls[0][0]).toBe('up migration completed, applied 1 migrations.')
47+
expect((console.info as jest.Mock).mock.calls[1][0]).toEqual({totalMigrations: 10})
48+
})
49+
50+
test('unknown events', () => {
51+
prettyLogger.info({event: 'finished', message: 'process finished.', durationSeconds: 0.1})
52+
53+
expect((console.info as jest.Mock).mock.calls[0][0]).toEqual({
54+
event: 'finished',
55+
message: 'process finished.',
56+
durationSeconds: 0.1,
57+
})
58+
})
59+
60+
test('invalid parameters', () => {
61+
prettyLogger.info(null as any)
62+
prettyLogger.info(undefined as any)
63+
prettyLogger.info(0 as any)
64+
prettyLogger.info('1' as any)
65+
66+
expect((console.info as jest.Mock).mock.calls).toEqual([[null], [undefined], [0], ['1']])
67+
})
68+
})

0 commit comments

Comments
 (0)