Skip to content

Commit

Permalink
add migration template
Browse files Browse the repository at this point in the history
  • Loading branch information
nealwp committed Oct 26, 2024
1 parent d034671 commit c40eb4d
Show file tree
Hide file tree
Showing 16 changed files with 645 additions and 118 deletions.
6 changes: 3 additions & 3 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ services:
- 5432:5432
environment:
- DEBUG=true
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=password
- POSTGRES_DB=sequelize-sandbox
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=sandbox-db
14 changes: 14 additions & 0 deletions example-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { QueryInterface } from 'sequelize';
import { MigrationParams } from 'umzug'
import { DataType } from 'sequelize-typescript';

type Migration = (params: MigrationParams<QueryInterface>) => Promise<unknown>;

export const up: Migration = async ({ context: queryInterface }) => {
//await queryInterface.createSchema()
//await queryInterface.createTable();
//await queryInterface.addColumn();
//await queryInterface.changeColumn();
//await queryInterface.removeColumn();
//await queryInterface.dropTable();
};
20 changes: 20 additions & 0 deletions migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Umzug, SequelizeStorage } from 'umzug';
import fs from 'node:fs';
import { client } from './src/db';

const umzug = new Umzug({
migrations: { glob: './src/migrations/*.ts' },
context: client.getQueryInterface(),
storage: new SequelizeStorage({ sequelize: client }),
logger: console, // log generated queries to console
create: {
template: filepath => [
[filepath, fs.readFileSync('example-migration.ts').toString()]
],
folder: './src/migrations',
}
});

if (require.main === module) {
umzug.runAsCLI().then(() => process.exit());
}
451 changes: 427 additions & 24 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"check": "tsc --noEmit",
"lint": "npx prettier --check .",
"format": "npx prettier --write .",
"db:start": "docker compose up -d",
"db:stop": "docker compose down"
"db": "docker compose up -d && npm run db:up",
"db:up": "ts-node migration up",
"db:stop": "docker compose down",
"migrate": "ts-node migration create --name "
},
"author": "nealwp",
"license": "ISC",
Expand All @@ -36,6 +38,7 @@
"reflect-metadata": "^0.2.2",
"sequelize": "^6.37.5",
"sequelize-typescript": "^2.1.6",
"typescript": "^5.6.3"
"typescript": "^5.6.3",
"umzug": "^3.8.2"
}
}
10 changes: 8 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import * as db from './db';
import router from './routes';
import { createServer } from './server';
import express from 'express';

const PORT = 3000;
const server = createServer(router);

const server = express();

server.use(express.json());
server.use(express.urlencoded({ extended: true }));
server.use(router);

db.initialize().then(async () => {
await db.runMigrations();
server.listen(PORT, () => {
console.log(`server listening on port ${PORT}`);
});
Expand Down
81 changes: 46 additions & 35 deletions src/db.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
import { Sequelize, SequelizeOptions } from 'sequelize-typescript';
import * as models from './models';

const config = {
DB_URL: 'localhost',
DB_USER: 'postgres',
DB_PASS: 'postgres',
DB_PORT: 5432,
DB_NAME: 'scratch',
};

const dbConfig: SequelizeOptions = {
dialect: 'postgres',
host: config.DB_URL,
username: config.DB_USER,
password: config.DB_PASS,
port: config.DB_PORT,
database: config.DB_NAME,
logging: false,
models: Object.values(models),
};

const sequelize = new Sequelize(dbConfig);

const initialize = async () => {
try {
await sequelize.authenticate();
await sequelize.sync({ alter: true });
console.log('All models were synchronized successfully.');
} catch (error) {
console.error(`Error initializing database: ${error}`);
}
};

export { initialize, sequelize as client };
import { ModelAttributeColumnOptions } from 'sequelize';
import { Sequelize, SequelizeOptions } from 'sequelize-typescript';
import { Umzug, SequelizeStorage } from 'umzug';
import * as models from './models';

// this is to make explicit column names required, instead of
// allowing Sequelize to auto-generate them
export type ColumnOptions<T> = Record<keyof T, ModelAttributeColumnOptions & { field: string}>;

const DB_HOST = 'localhost';
const DB_USER = 'postgres';
const DB_PASS = 'postgres';
const DB_PORT = 5432;
const DB_NAME = 'sandbox-db';

const dbConfig: SequelizeOptions = {
dialect: 'postgres',
host: DB_HOST,
username: DB_USER,
password: DB_PASS,
port: DB_PORT,
database: DB_NAME,
logging: false,
models: Object.values(models),
};

const sequelize = new Sequelize(dbConfig);

async function initialize() {
return sequelize.authenticate();
};

const MIGRATION_GLOB = process.env['NODE_ENV'] == 'production' ? `./dist/migrations/*.js` : `./src/migrations/*.ts`;

const umzug = new Umzug({
migrations: { glob: MIGRATION_GLOB },
context: sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize }),
logger: console, // log generated queries to console
});

async function runMigrations() {
return umzug.up();
}

export { initialize, runMigrations, sequelize as client };
13 changes: 13 additions & 0 deletions src/migrations/2024.10.26T20.56.44.create-character-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { QueryInterface } from 'sequelize';
import { MigrationParams } from 'umzug';
import { DataType } from 'sequelize-typescript';

type Migration = (params: MigrationParams<QueryInterface>) => Promise<unknown>;

export const up: Migration = async ({ context: queryInterface }) => {
await queryInterface.createTable('character', {
id: { field: 'id', type: DataType.INTEGER, autoIncrementIdentity: true, primaryKey: true },
name: { field: 'name', type: DataType.STRING },
age: { field: 'age', type: DataType.INTEGER },
});
};
16 changes: 16 additions & 0 deletions src/migrations/2024.10.26T21.11.33.create-inventory-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { QueryInterface } from 'sequelize';
import { MigrationParams } from 'umzug'
import { DataType } from 'sequelize-typescript';

type Migration = (params: MigrationParams<QueryInterface>) => Promise<unknown>;

export const up: Migration = async ({ context: queryInterface }) => {
await queryInterface.createTable('inventory', {
id: { field: 'id', type: DataType.INTEGER, autoIncrementIdentity: true, primaryKey: true },
characterId: {
field: 'character_id',
type: DataType.INTEGER,
references: { model: 'character', key: 'id' },
},
});
};
15 changes: 15 additions & 0 deletions src/migrations/2024.10.26T21.52.09.create-weapon-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { QueryInterface } from 'sequelize';
import { MigrationParams } from 'umzug'
import { DataType } from 'sequelize-typescript';

type Migration = (params: MigrationParams<QueryInterface>) => Promise<unknown>;

export const up: Migration = async ({ context: queryInterface }) => {
await queryInterface.createTable('weapon', {
id: { field: 'id', type: DataType.INTEGER, autoIncrementIdentity: true, primaryKey: true },
inventoryId: { field: 'inventory_id', type: DataType.INTEGER, references: { model: 'inventory', key: 'id' }},
name: { field: 'name', type: DataType.STRING },
damage: { field: 'damage', type: DataType.FLOAT },
type: { field: 'type', type: DataType.STRING },
});
};
4 changes: 4 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// use this file to "register" models, and to avoid having a huge stack of imports in db.ts
export { Weapon } from './models/weapon.model';
export { Character } from './models/character.model';
export { Inventory } from './models/inventory.model';
31 changes: 22 additions & 9 deletions src/models/character.model.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { Table, Column, Model, PrimaryKey, AutoIncrement, HasMany } from 'sequelize-typescript';
import { CharacterAttributes, CharacterCreationAttributes } from '../@types/character.types';
import { ColumnOptions } from '../db';
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
import { Inventory } from './inventory.model';

@Table({ tableName: 'characters' })
class Character extends Model<CharacterAttributes, CharacterCreationAttributes> implements CharacterAttributes {
@PrimaryKey
@AutoIncrement
@Column
interface CreationAttributes {
name: string;
age: number;
}

interface Attributes extends CreationAttributes {
id: number;
}

const columns: ColumnOptions<Attributes> = {
id: { field: 'id', type: DataType.INTEGER, autoIncrementIdentity: true, primaryKey: true },
name: { field: 'name', type: DataType.STRING },
age: { field: 'age', type: DataType.INTEGER },
}

@Table({ tableName: 'character' })
class Character extends Model<Attributes, CreationAttributes> implements Attributes {
@Column(columns.id)
override id!: number;

@Column
@Column(columns.name)
name!: string;

@Column
@Column(columns.age)
age!: number;

@HasMany(() => Inventory)
Expand Down
3 changes: 0 additions & 3 deletions src/models/index.ts

This file was deleted.

32 changes: 22 additions & 10 deletions src/models/inventory.model.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
import { Table, Column, Model, PrimaryKey, AutoIncrement, ForeignKey, BelongsTo, HasMany } from 'sequelize-typescript';
import { InventoryAttributes, InventoryCreationAttributes } from '../@types/inventory.types';
import { Table, Column, Model, ForeignKey, HasMany, DataType } from 'sequelize-typescript';
import { ColumnOptions } from '../db';
import { Character } from './character.model';
import { Weapon } from './weapon.model';

interface CreationAttributes {
characterId: number;
}

interface Attributes extends CreationAttributes {
id: number;
}

const columns: ColumnOptions<Attributes> = {
id: { field: 'id', type: DataType.INTEGER, autoIncrementIdentity: true, primaryKey: true },
characterId: {
field: 'character_id',
type: DataType.INTEGER,
references: { model: 'character', key: 'id' },
},
}

@Table({ tableName: 'inventory' })
class Inventory extends Model<InventoryAttributes, InventoryCreationAttributes> implements InventoryAttributes {
@PrimaryKey
@AutoIncrement
@Column
class Inventory extends Model<Attributes, CreationAttributes> implements Attributes {
@Column(columns.id)
override id!: number;

@ForeignKey(() => Character)
@Column
@Column(columns.characterId)
characterId!: number;

@BelongsTo(() => Character)
character!: Character;

@HasMany(() => Weapon)
weapons!: Weapon[];
}
Expand Down
44 changes: 29 additions & 15 deletions src/models/weapon.model.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
import { Table, Column, Model, PrimaryKey, AutoIncrement, ForeignKey, BelongsTo } from 'sequelize-typescript';
import { WeaponAttributes, WeaponCreationAttributes, WeaponType } from '../@types/weapon.types';
import { Table, Column, Model, ForeignKey, DataType } from 'sequelize-typescript';
import { ColumnOptions } from '../db';
import { Inventory } from './inventory.model';

@Table({ tableName: 'weapons' })
class Weapon extends Model<WeaponAttributes, WeaponCreationAttributes> implements WeaponAttributes {
@PrimaryKey
@AutoIncrement
@Column
interface CreationAttributes {
inventoryId: number;
name: string;
damage: number;
type: string;
}

interface Attributes extends CreationAttributes {
id: number;
}

const columns: ColumnOptions<Attributes> = {
id: { field: 'id', type: DataType.INTEGER, autoIncrementIdentity: true, primaryKey: true },
inventoryId: { field: 'inventory_id', type: DataType.INTEGER, references: { model: 'inventory', key: 'id' }},
name: { field: 'name', type: DataType.STRING },
damage: { field: 'damage', type: DataType.FLOAT },
type: { field: 'type', type: DataType.STRING },
}

@Table({ tableName: 'weapon' })
class Weapon extends Model<Attributes, CreationAttributes> implements Attributes {
@Column(columns.id)
override id!: number;

@ForeignKey(() => Inventory)
@Column
@Column(columns.inventoryId)
inventoryId!: number;

@BelongsTo(() => Inventory)
inventory!: Inventory;

@Column
@Column(columns.name)
name!: string;

@Column
@Column(columns.damage)
damage!: number;

@Column
type!: WeaponType;
@Column(columns.type)
type!: string;
}

export { Weapon };
14 changes: 0 additions & 14 deletions src/server.ts

This file was deleted.

0 comments on commit c40eb4d

Please sign in to comment.