Skip to content

Commit

Permalink
update cli
Browse files Browse the repository at this point in the history
  • Loading branch information
zfben committed Feb 2, 2025
1 parent 8447ed4 commit 3f28a11
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 22 deletions.
14 changes: 12 additions & 2 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,18 @@ describe('escapeValue', () => {
expect(escapeValue([[1, 2], ['a']])).toBe('ARRAY[ARRAY[1,2],ARRAY[\'a\']]')
})

it('handles date values', () => {
const date = new Date('2023-01-01T00:00:00.000Z')
expect(escapeValue(date)).toBe('\'2023-01-01T00:00:00.000Z\'')
})

it('handles object values', () => {
expect(escapeValue({ foo: 'bar' })).toBe('\'{"foo":"bar"}\'')
expect(escapeValue({ a: 1, b: true })).toBe('\'{"a":1,"b":true}\'')
})

it('throws error for unsupported types', () => {
expect(() => escapeValue({})).toThrowError('Unsupported value type: [object Object]')
expect(() => escapeValue(undefined)).toThrowError('Unsupported value type: undefined')
expect(() => escapeValue(undefined)).toThrowError('Unsupported value: undefined')
expect(() => escapeValue(() => 1)).toThrowError('Unsupported value: () => 1')
})
})
205 changes: 192 additions & 13 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import postgres, { type Sql } from "postgres"
import { Logger } from '@faasjs/logger'
import { existsSync, globSync, mkdirSync, writeFileSync } from 'node:fs'
import { resolve, join, basename } from 'node:path'
import { SchemaBuilder } from "../schema-builder"
import { createClient } from "../client"

function createMigrationTable(sql: Sql) {
return sql`CREATE TABLE IF NOT EXISTS typed_pg_migrations (
"name" varchar(255) NULL,
migration_time timestamptz NULL,
CONSTRAINT typed_pg_migrations_pkey PRIMARY KEY (name)
)`
}

export async function main(operation = process.argv[2] as string) {
const logger = new Logger('TypedPg')
Expand All @@ -22,29 +34,196 @@ export async function main(operation = process.argv[2] as string) {
}

if (!operation) {
logger.error('Please provide a operation to run: `typed-pg <operation>`, e.g. `typed-pg status`')
logger.error(`Please provide a operation to run: typed-pg <operation>
- status: Show the status of migrations
- migrate: Run all pending migrations
- up: Run the next migration
- down: Rollback the last migration
- new <name>: Create a new migration file with the given name
`)
return
}

switch (operation) {
case 'status': {
await sql`CREATE TABLE IF NOT EXISTS typed_pg_migrations_lock (
"index" serial4 NOT NULL,is_locked int4 NULL,CONSTRAINT typed_pg_migrations_lock_pkey PRIMARY KEY (index)
)`
await sql`CREATE TABLE IF NOT EXISTS typed_pg_migrations (
id serial4 NOT NULL,
"name" varchar(255) NULL,
batch int4 NULL,
migration_time timestamptz NULL,
CONSTRAINT typed_pg_migrations_pkey PRIMARY KEY (id)
)`
const lock = await sql`SELECT * FROM typed_pg_migrations_lock`
await createMigrationTable(sql)
const migrations = await sql`SELECT * FROM typed_pg_migrations`

logger.info('Status:')
logger.info('Lock:', lock)
logger.info('Migrations:', migrations)
break
}
case 'migrate': {
const folder = resolve('migrations')

if (!existsSync(folder)) {
logger.error('Migration folder not found:', folder)
return
}

const files = globSync(join(folder, '*.ts'))

if (!files.length) {
logger.error('No migration files found:', folder)
return
}

await createMigrationTable(sql)
const migrations = await sql`SELECT * FROM typed_pg_migrations`

const builder = new SchemaBuilder(createClient(sql))

for (const file of files) {
const name = basename(file).replace('.ts', '')
const up = (await import(file)).up

if (migrations.find((m: any) => m.name === name)) {
logger.debug('Migration already ran:', name)
continue
}

logger.info('Migrating:', name)

try {
up(builder)
await builder.run()

await sql`INSERT INTO typed_pg_migrations (name, migration_time) VALUES (${name}, NOW())`
} catch (error) {
logger.error('Migrate failed:', name, error)
return
}
}
break
}
case 'up': {
const folder = resolve('migrations')

if (!existsSync(folder)) {
logger.error('Migration folder not found:', folder)
return
}

const files = globSync(join(folder, '*.ts'))

if (!files.length) {
logger.error('No migration files found:', folder)
return
}

await createMigrationTable(sql)

const migrations = await sql`SELECT * FROM typed_pg_migrations ORDER BY migration_time DESC LIMIT 1`

const lastMigration = migrations[0]

const nextFile = files.find((file) => basename(file).replace('.ts', '') > lastMigration?.name)

if (!nextFile) {
logger.error('No pending migrations found')
return
}

const name = basename(nextFile).replace('.ts', '')

const builder = new SchemaBuilder(createClient(sql))

logger.info('Migrating:', name)

try {
const { up } = await import(nextFile)
up(builder)
await builder.run()

await sql`INSERT INTO typed_pg_migrations (name, migration_time) VALUES (${name}, NOW())`
} catch (error) {
logger.error('Migrate failed:', name, error)
return
}

break
}
case 'down': {
const folder = resolve('migrations')

if (!existsSync(folder)) {
logger.error('Migration folder not found:', folder)
return
}

const files = globSync(join(folder, '*.ts'))

if (!files.length) {
logger.error('No migration files found:', folder)
return
}

await createMigrationTable(sql)
const migrations = await sql`SELECT * FROM typed_pg_migrations ORDER BY migration_time DESC LIMIT 1`

const lastMigration = migrations[0]

if (!lastMigration) {
logger.error('No migrations found')
return
}

const file = join(folder, `${lastMigration.name}.ts`)

if (!existsSync(file)) {
logger.error('Migration file not found:', file)
return
}

const builder = new SchemaBuilder(createClient(sql))

logger.info('Rolling back:', lastMigration.name)

try {
const { down } = await import(file)
down(builder)
await builder.run()

await sql`DELETE FROM typed_pg_migrations WHERE name = ${lastMigration.name} LIMIT 1`
} catch (error) {
logger.error('Rollback failed:', lastMigration.name, error)
return
}

break
}
case 'new': {
const name = process.argv[3] as string

if (!name) {
logger.error('Please provide a name for the migration: `typed-pg new <name>`')
return
}

const folder = resolve('migrations')
const filename = `${new Date().toISOString().replace(/[^0-9]/g, '')}-${name}.ts`
const file = join(folder, filename)

if (!existsSync(folder))
mkdirSync(folder, { recursive: true })
writeFileSync(file, `
// ${filename}.ts
import type { SchemaBuilder } from '@typed-pg/schema-builder'
export function up(builder: SchemaBuilder) {
// Write your migration here
}
export function down(builder: SchemaBuilder) {
// Write your rollback here
}
`)

logger.info('Created migration:', file)
break
}
default:
logger.error('Unknown operation:', operation)
break
}
}
12 changes: 6 additions & 6 deletions src/schema-builder/table-builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { escapeIdentifier } from "../utils"
import { escapeIdentifier, escapeValue } from "../utils"

type ColumnType = 'smallint'
| 'integer'
Expand Down Expand Up @@ -242,11 +242,11 @@ export class TableBuilder {
escapeIdentifier(name),
def.type,
def.nullable ? 'NULL' : 'NOT NULL',
def.defaultValue !== undefined ? `DEFAULT ${def.defaultValue}` : '',
def.primary ? 'PRIMARY KEY' : '',
def.unique ? 'UNIQUE' : '',
def.defaultValue !== undefined ? `DEFAULT ${escapeValue(def.defaultValue)}` : null,
def.primary ? 'PRIMARY KEY' : null,
def.unique ? 'UNIQUE' : null,
def.references ?
`REFERENCES ${def.references.table}(${def.references.column})` : '',
`REFERENCES ${def.references.table}(${def.references.column})` : null,
].filter(Boolean)

return parts.join(' ')
Expand All @@ -263,7 +263,7 @@ export class TableBuilder {
case 'nullable':
return `ALTER COLUMN ${escapeIdentifier(columnName)} ${value ? 'DROP' : 'SET'} NOT NULL;`
case 'defaultValue':
return `ALTER COLUMN ${escapeIdentifier(columnName)} SET DEFAULT ${value};`
return `ALTER COLUMN ${escapeIdentifier(columnName)} SET DEFAULT ${escapeValue(value)};`
case 'primary':
return value ? `ADD PRIMARY KEY (${escapeIdentifier(columnName)})` : `DROP CONSTRAINT IF EXISTS ${escapeIdentifier(`${this.tableName}_pkey`)};`
case 'unique':
Expand Down
10 changes: 9 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ export function escapeValue(value: any): string {
return `ARRAY[${value.map(escapeValue).join(',')}]`
}

throw Error(`Unsupported value type: ${value}`)
if (value instanceof Date) {
return `'${value.toISOString()}'`
}

if (typeof value === 'object') {
return `'${JSON.stringify(value).replace(/'/g, "''")}'`
}

throw Error(`Unsupported value: ${value}`)
}

export function createTemplateStringsArray(str: string): TemplateStringsArray {
Expand Down

0 comments on commit 3f28a11

Please sign in to comment.