Skip to content

Commit

Permalink
Added platformatic example
Browse files Browse the repository at this point in the history
Signed-off-by: Matteo Collina <[email protected]>
  • Loading branch information
mcollina committed Mar 6, 2024
1 parent 637aacb commit 0ab148a
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 6 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.0.6",
"description": "Run migrations code based on semantic version rules",
"scripts": {
"test": "borp",
"test": "borp --coverage",
"clean": "rm -rf dist",
"build": "npm run clean && tsc -p tsconfig.build.json",
"lint": "prettier --check .",
Expand Down Expand Up @@ -35,6 +35,7 @@
"@matteo.collina/tspl": "^0.1.1",
"@types/node": "^20.11.24",
"borp": "^0.9.1",
"desm": "^1.3.1",
"prettier": "^3.2.5",
"typescript": "^5.3.3"
},
Expand Down
65 changes: 60 additions & 5 deletions src/lib/semgrator.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,87 @@
import semver from 'semver'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { readdir } from 'node:fs/promises'

export type Migration<Input, Output = Input> = {
version: string
toVersion?: string
up: (input: Input) => Promise<Output> | Output
}

interface SemgratorParams<Input> {
interface BaseSemgratorParams<Input> {
version: string
migrations: Migration<any, any>[]
input: Input
}

interface SemgratorParamsWithMigrations<Input>
extends BaseSemgratorParams<Input> {
migrations: Migration<Input>[]
}

interface SemgratorParamsWithPath<Input>
extends BaseSemgratorParams<Input> {
path: string
}

interface SemgratorResult<Output> {
version: string
result: Output
}

export async function semgrator<Input = unknown, Output = unknown>(
params: SemgratorParams<Input>,
async function semgratorWithMigrations<Input, Output>(
params: SemgratorParamsWithMigrations<Input>,
): Promise<SemgratorResult<Output>> {
let result = params.input as unknown
let lastVersion = params.version
for (const migration of params.migrations) {
if (semver.gt(migration.version, lastVersion)) {
// @ts-expect-error
result = await migration.up(result)
lastVersion = migration.version
lastVersion = migration.toVersion || migration.version
}
}

return { version: lastVersion, result: result as Output }
}

async function loadMigrationsFromPath<Input>(
path: string,
): Promise<Migration<Input>[]> {
const files = (await readdir(path)).filter(file =>
file.match(/\.(c|m)?js$/),
)

const migrations = await Promise.all(
files.map(async file => {
const module = await import(
pathToFileURL(join(path, file)).toString()
)
return module.migration as Migration<Input>
}),
)

migrations.sort((a, b) => semver.compare(a.version, b.version))

return migrations
}

export async function semgrator<Input = unknown, Output = unknown>(
params:
| SemgratorParamsWithPath<Input>
| SemgratorParamsWithMigrations<Input>,
): Promise<SemgratorResult<Output>> {
if ('path' in params) {
const migrations = await loadMigrationsFromPath<Input>(
params.path,
)
return semgratorWithMigrations<Input, Output>({
...params,
migrations,
})
} else if ('migrations' in params) {
return semgratorWithMigrations<Input, Output>(params)
} else {
throw new Error('Specify either path or migrations')
}
}
3 changes: 3 additions & 0 deletions src/test/fixtures/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type Order = {
order: string[]
}
10 changes: 10 additions & 0 deletions src/test/fixtures/plt/0.16.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Migration } from '../../../lib/semgrator.js'
import type { Order } from '../order.js'

export const migration: Migration<Order> = {
version: '0.16.0',
up: (input: Order) => {
input.order.push('0.16.0')
return input
},
}
10 changes: 10 additions & 0 deletions src/test/fixtures/plt/0.17.x.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Migration } from '../../../lib/semgrator.js'
import type { Order } from '../order.js'

export const migration: Migration<Order> = {
version: '0.17.0',
up: (input: Order) => {
input.order.push('0.17.0')
return input
},
}
11 changes: 11 additions & 0 deletions src/test/fixtures/plt/1.x.x.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Migration } from '../../../lib/semgrator.js'
import type { Order } from '../order.js'

export const migration: Migration<Order> = {
version: '1.0.0',
toVersion: '1.42.0',
up: (input: Order) => {
input.order.push('1.0.0')
return input
},
}
10 changes: 10 additions & 0 deletions src/test/fixtures/plt/from-zero-eighteen-to-will-see.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Migration } from '../../../lib/semgrator.js'
import type { Order } from '../order.js'

export const migration: Migration<Order> = {
version: '0.18.0',
up: (input: Order) => {
input.order.push('0.18.0')
return input
},
}
21 changes: 21 additions & 0 deletions src/test/plt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test } from 'node:test'
import { semgrator } from '../lib/semgrator.js'
import { equal } from 'node:assert/strict'
import { join } from 'desm'
import type { Order } from './fixtures/order.js'
import { deepEqual } from 'node:assert/strict'

test('apply all migrations in a folder', async t => {
const res = await semgrator<Order>({
version: '0.15.0',
path: join(import.meta.url, 'fixtures', 'plt'),
input: {
order: [],
},
})

equal(res.version, '1.42.0')
deepEqual(res.result, {
order: ['0.16.0', '0.17.0', '0.18.0', '1.0.0'],
})
})
75 changes: 75 additions & 0 deletions src/test/semgrator.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { test } from 'node:test'
import { semgrator, Migration } from '../lib/semgrator.js'
import { tspl } from '@matteo.collina/tspl'
import { rejects } from 'node:assert/strict'

type SeenBy = {
foo: string
Expand Down Expand Up @@ -126,3 +127,77 @@ test('apply no migrations', async t => {
foo: 'bar',
})
})

test('toVersion', async t => {
const plan = tspl(t, { plan: 3 })
const m1: Migration<SeenBy> = {
version: '2.0.0',
async up(input) {
return {
...input,
seenBy: '2.0.0',
}
},
}

const m2: Migration<SeenBy> = {
version: '2.4.0',
toVersion: '2.10.0',
up(input) {
plan.deepEqual(input, {
foo: 'bar',
seenBy: '2.0.0',
})
return {
...input,
seenBy: '2.4.0',
}
},
}

const res = await semgrator<SeenBy, SeenBy>({
version: '1.0.0',
migrations: [m1, m2],
input: {
foo: 'bar',
},
})

plan.equal(res.version, '2.10.0')
plan.deepEqual(res.result, {
foo: 'bar',
seenBy: '2.4.0',
})
})

test('throws if path or migrations are missing', async t => {
await rejects(
// @ts-expect-error
semgrator({ version: '1.0.0' }),
{ message: 'Specify either path or migrations' },
)
})

test('throws if version is missing', async t => {
const m1: Migration<SeenBy> = {
version: '2.0.0',
async up(input) {
return input
},
}

const m2: Migration<SeenBy> = {
version: '2.4.0',
up(input) {
return input
},
}
await rejects(
// @ts-expect-error
semgrator({ migrations: [m1, m2] }),
{
message:
'Invalid version. Must be a string. Got type "undefined".',
},
)
})

0 comments on commit 0ab148a

Please sign in to comment.