Skip to content

Commit e346363

Browse files
authored
Socket.io & Sequelize Migrations (#7)
* socket.io server setup for express and nest * socket-io express server setup * socket to do in react & next app * socket-io wip * socket io server wip * socket.io in next-client * socket api test wip * socket-io done * wip * migrations wip * seeders wip * seeders wip * seed cars * update comment * mysql migrations wip * mysql migrations wip * update comands.md files * migrations wip * mysql migrations wip * migration changes wip * seed mysql db, handle case for optional values * add scripts to seeder pkgs * fix car list api * minor lint fix * generate migation file cmd * def buyer model * def buyerRoute * add buyers migration & seeder * isUUIDv6 validation fn * more exmaple with sequelize include * add seeder and migration storage notes * indent docs
1 parent 6690545 commit e346363

File tree

80 files changed

+2136
-381
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+2136
-381
lines changed

.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mongo-seeders
2+
mysql-migrations
3+
postgres-migrations

.gitignore

+5-1
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,8 @@ coverage
4444

4545
# misc
4646
apps/express-server/public
47-
apps/express-server/uploads
47+
apps/express-server/uploads
48+
49+
# config files for migrations
50+
apps/mysql-migrations/config/config.json
51+
apps/postgres-migrations/config/config.json

apps/express-server/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"pg": "^8.13.1",
2525
"pg-hstore": "^2.3.4",
2626
"sequelize": "^6.37.5",
27+
"socket.io": "^4.8.1",
2728
"stytch": "^12.4.0",
29+
"uuid": "^11.0.5",
2830
"winston": "3.11.0"
2931
},
3032
"devDependencies": {
@@ -35,6 +37,7 @@
3537
"@types/fluent-ffmpeg": "^2.1.24",
3638
"@types/multer": "1.4.12",
3739
"@types/node": "^20.11.17",
40+
"@types/uuid": "^10.0.0",
3841
"env-cmd": "^10.1.0",
3942
"eslint": "^8.57.0",
4043
"nodemon": "^3.0.3",

apps/express-server/src/app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ExpressServerEndpoints } from '@csl/react-express';
55
import { ENV_VARS, ServerConfig } from '@/app-constants';
66
import { requestLogger } from '@/middleware';
77
import { routesArray } from '@/routes';
8+
import { io } from '.';
89

910
const app: Express = express();
1011

@@ -41,6 +42,7 @@ app.use(requestLogger);
4142
app.use(express.static(path.join(__dirname, '../public')));
4243

4344
app.get('/', (_: Request, response: Response) => {
45+
io.emit('noArg');
4446
response.status(200).json({
4547
env: ENV_VARS.env,
4648
message: 'Api is up & running!!!'

apps/express-server/src/db/mysql/index.ts

+2-10
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,10 @@ export async function connectMySQLDB() {
3131
`[ ⚡️ ${hostName} ⚡️ ] - Connected to MySQL DB`
3232
);
3333

34-
if (ENV_VARS.env === 'production') {
35-
await mySQLSequelize.sync();
36-
console.log('Production: Run migrations for schema updates.');
37-
} else {
38-
await mySQLSequelize.sync({ alter: true });
39-
console.log(`${ENV_VARS.env}: MySQL Database schema synchronized with alter mode.`);
40-
}
41-
4234
/**
4335
* sequelize.close() -> will close the connection to the DB.
44-
* You will need to create a new Sequelize instance to
45-
* access your database again.
36+
* You will need to create a new Sequelize instance to
37+
* access your database again.
4638
*/
4739
} catch (error) {
4840
winstonLogger.error('⚠ Error connecting to MySQL Database ⚠', error);
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export * from './person';
1+
export * from './user';

apps/express-server/src/db/mysql/models/person.ts apps/express-server/src/db/mysql/models/user.ts

+13-17
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
/* eslint-disable no-use-before-define */
22

33
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
4-
import { mySQLSequelize, shouldAlterTable } from '@/db/mysql';
4+
import { mySQLSequelize } from '@/db/mysql';
55

66
export type UserModelAttributes = InferAttributes<UserModel>;
77
export type UserModelCreationAttributes = InferCreationAttributes<UserModel>;
88

99
class UserModel extends Model<UserModelAttributes, UserModelCreationAttributes> {
1010
declare id: CreationOptional<number>;
11-
name!: string;
12-
email!: string;
13-
isActive!: CreationOptional<boolean>;
14-
preferences!: object;
15-
tags!: string[];
16-
age!: number;
17-
createdAt!: CreationOptional<Date>;
18-
updatedAt!: CreationOptional<Date>;
11+
declare name: string;
12+
declare email: string;
13+
declare isActive: CreationOptional<boolean>;
14+
declare preferences: object | null;
15+
declare tags: string[] | null;
16+
declare age: number;
17+
declare createdAt: CreationOptional<Date>;
18+
declare updatedAt: CreationOptional<Date>;
1919
}
2020

2121
UserModel.init(
@@ -43,12 +43,14 @@ UserModel.init(
4343
},
4444
preferences: {
4545
type: DataTypes.JSON,
46-
allowNull: true
46+
allowNull: true,
47+
defaultValue: null
4748
},
4849
/* Arrays are not supported in MySQL */
4950
tags: {
5051
type: DataTypes.JSON,
51-
allowNull: true
52+
allowNull: true,
53+
defaultValue: null
5254
},
5355
age: {
5456
type: DataTypes.INTEGER,
@@ -93,12 +95,6 @@ UserModel.init(
9395
}
9496
);
9597

96-
async function createTable() {
97-
await UserModel.sync({ alter: shouldAlterTable });
98-
}
99-
100-
createTable();
101-
10298
const InactiveUserModel = UserModel.scope('inactiveUsers');
10399

104100
export { UserModel, InactiveUserModel };

apps/express-server/src/db/postgres/index.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,20 @@ export const postgreSequelize = new Sequelize(ENV_VARS.postgresUrl, {
2626

2727
export async function connectPostgresDB() {
2828
try {
29+
/**
30+
* SELECT 1+1 AS result is the default test query in Sequelize
31+
* to verify database connection, when calling sequelize.authenticate().
32+
* It can't be disabled though.
33+
*/
2934
await postgreSequelize.authenticate();
3035
winstonLogger.info(
3136
`[ ⚡️ ${hostName} ⚡️ ] - Connected to Postgres`
3237
);
3338

34-
if (ENV_VARS.env === 'production') {
35-
await postgreSequelize.sync();
36-
console.log('Production: Run migrations for schema updates.');
37-
} else {
38-
await postgreSequelize.sync({ alter: true });
39-
console.log(`${ENV_VARS.env}: Postgres Database schema synchronized with alter mode.`);
40-
}
41-
4239
/**
4340
* sequelize.close() -> will close the connection to the DB.
44-
* You will need to create a new Sequelize instance to
45-
* access your database again.
41+
* You will need to create a new Sequelize instance to
42+
* access your database again.
4643
*/
4744
} catch (error) {
4845
winstonLogger.error('⚠ Error connecting to Postgres Database ⚠');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/* eslint-disable no-use-before-define */
2+
3+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
4+
import { v6 as UUIDv6 } from 'uuid';
5+
import { postgreSequelize } from '@/db/postgres';
6+
import { isUUIDv6 } from '@/utils';
7+
import { CarModel, CarColors } from './car';
8+
9+
export type BuyerModelAttributes = InferAttributes<BuyerModel>;
10+
export type BuyerModelCreationAttributes = InferCreationAttributes<BuyerModel>;
11+
12+
class BuyerModel extends Model<BuyerModelAttributes, BuyerModelCreationAttributes> {
13+
declare id: CreationOptional<string>;
14+
declare name: string;
15+
declare car_id: string;
16+
declare color: CarColors;
17+
declare purchased_on: CreationOptional<Date>;
18+
}
19+
20+
BuyerModel.init(
21+
{
22+
id: {
23+
type: DataTypes.UUID,
24+
defaultValue: () => UUIDv6(),
25+
primaryKey: true,
26+
validate: {
27+
isuuidV6(value: string) {
28+
if (!isUUIDv6(value)) {
29+
throw new Error('Invalid UUID v6 format.');
30+
}
31+
}
32+
}
33+
},
34+
name: {
35+
type: DataTypes.STRING,
36+
allowNull: false,
37+
},
38+
car_id: {
39+
type: DataTypes.UUID,
40+
allowNull: false,
41+
references: {
42+
model: CarModel,
43+
key: 'id',
44+
},
45+
validate: {
46+
isuuidV6(value: string) {
47+
if (!isUUIDv6(value)) {
48+
throw new Error('Invalid UUID v6 format.');
49+
}
50+
}
51+
},
52+
},
53+
color: {
54+
type: DataTypes.ENUM(...Object.values(CarColors)),
55+
allowNull: false,
56+
},
57+
purchased_on: {
58+
type: DataTypes.DATE,
59+
allowNull: false,
60+
defaultValue: DataTypes.NOW,
61+
}
62+
},
63+
{
64+
sequelize: postgreSequelize,
65+
timestamps: true,
66+
createdAt: 'purchased_on',
67+
updatedAt: false,
68+
modelName: 'buyer',
69+
}
70+
);
71+
72+
BuyerModel.belongsTo(CarModel, {
73+
foreignKey: 'car_id',
74+
as: 'carDetails',
75+
onDelete: 'CASCADE',
76+
onUpdate: 'CASCADE'
77+
});
78+
79+
CarModel.hasMany(BuyerModel, {
80+
foreignKey: 'car_id',
81+
as: 'owners',
82+
});
83+
84+
export { BuyerModel };

apps/express-server/src/db/postgres/models/car.ts

+51-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable no-use-before-define */
22

33
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
4-
import { postgreSequelize, shouldAlterTable } from '@/db/postgres';
4+
import { v6 as UUIDv6 } from 'uuid';
5+
import { postgreSequelize } from '@/db/postgres';
6+
import { isUUIDv6 } from '@/utils';
57
import { CarBrandModel } from './car-brand';
68

79
export enum CarColors {
@@ -26,6 +28,16 @@ export enum CarColors {
2628
export type CarModelAttributes = InferAttributes<CarModel>;
2729
export type CarModelCreationAttributes = InferCreationAttributes<CarModel>;
2830

31+
/**
32+
* Always create and modify tables using migration scripts instead of
33+
* calling the createTable method, because everytime you restart the
34+
* server, the same index will be called multiple times, which will
35+
* eventually give you "Too many keys specified; max 64 keys allowed"
36+
* error.
37+
*
38+
* Migrations can be easily replicated on different environments and should
39+
* be the preferred way of creating and managing tables.
40+
*/
2941
class CarModel extends Model<CarModelAttributes, CarModelCreationAttributes> {
3042
declare id: CreationOptional<string>;
3143
name!: string;
@@ -44,9 +56,25 @@ class CarModel extends Model<CarModelAttributes, CarModelCreationAttributes> {
4456
CarModel.init(
4557
{
4658
id: {
59+
/**
60+
* FYI UUIDv6 must be used instead of v4, as v6 is
61+
* Timestamp-based + random whereas v4 is fully random,
62+
* thus its better used for indexing. Previously it was
63+
* DataTypes.UUIDV4.
64+
*
65+
* Also use DataTypes.UUID only instead of DataTypes.STRING as
66+
* the former is lighter and faster in indexing.
67+
*/
4768
type: DataTypes.UUID,
48-
defaultValue: DataTypes.UUIDV4,
49-
primaryKey: true
69+
defaultValue: () => UUIDv6(),
70+
primaryKey: true,
71+
validate: {
72+
isuuidV6(value: string) {
73+
if (!isUUIDv6(value)) {
74+
throw new Error('Invalid UUID v6 format.');
75+
}
76+
}
77+
}
5078
},
5179
name: {
5280
type: DataTypes.STRING,
@@ -108,21 +136,30 @@ CarModel.init(
108136
);
109137
},
110138
},
111-
created_at: DataTypes.DATE,
112-
updated_at: DataTypes.DATE,
139+
created_at: {
140+
type: DataTypes.DATE,
141+
allowNull: false,
142+
defaultValue: DataTypes.NOW,
143+
},
144+
updated_at: {
145+
type: DataTypes.DATE,
146+
allowNull: false,
147+
defaultValue: DataTypes.NOW,
148+
}
113149
},
114150
{
115151
sequelize: postgreSequelize,
116152
/**
117153
* This will prevent the auto-pluralization performed by Sequelize,
118154
* ie. the table name will be equal to the model name, without
119155
* any modifications
156+
*
157+
* freezeTableName: true,
120158
*/
121-
freezeTableName: true,
159+
timestamps: true,
122160
createdAt: 'created_at',
123161
updatedAt: 'updated_at',
124162
modelName: 'car',
125-
timestamps: true,
126163
/**
127164
* Sequelize provides paranoid tables which soft deletes a record
128165
* by inserting deletedAt timestamp. Timestamps must be enabled to
@@ -157,7 +194,10 @@ CarModel.init(
157194
}
158195
);
159196

160-
/* Define the relationship between CarModel and CarBrandModel using optional aliases */
197+
/**
198+
* Define the relationship between CarModel and CarBrandModel
199+
* using optional aliases
200+
*/
161201
CarModel.belongsTo(CarBrandModel, {
162202
foreignKey: 'brand_id',
163203
as: 'brand',
@@ -167,7 +207,7 @@ CarModel.belongsTo(CarBrandModel, {
167207

168208
CarBrandModel.hasMany(CarModel, {
169209
foreignKey: 'brand_id',
170-
as: 'cars',
210+
as: 'carModels',
171211
});
172212

173213

@@ -176,12 +216,9 @@ CarBrandModel.hasMany(CarModel, {
176216
* if it exists. The former option will drop the table while the
177217
* latter performs the necessary changes in the table to make it
178218
* match the model.
219+
*
220+
* await CarModel.sync({ alter: shouldAlterTable });
179221
*/
180-
async function createTable() {
181-
await CarModel.sync({ alter: shouldAlterTable });
182-
}
183-
184-
createTable();
185222

186223
export { CarModel };
187224

Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export * from './buyer';
12
export * from './car-brand';
23
export * from './car';

0 commit comments

Comments
 (0)