Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into user-wallet-integrati…
Browse files Browse the repository at this point in the history
…on-tests
  • Loading branch information
iamacook committed Feb 13, 2025
2 parents 5b119ed + 7c65cf5 commit 5955044
Show file tree
Hide file tree
Showing 53 changed files with 2,118 additions and 375 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ jobs:
- run: |
BUILD_NUMBER=${{ github.sha }}
echo "BUILD_NUMBER=${BUILD_NUMBER::7}" >> "$GITHUB_ENV"
- uses: docker/setup-qemu-action@v3.3.0
- uses: docker/setup-qemu-action@v3.4.0
with:
platforms: arm64
- uses: docker/setup-buildx-action@v3
Expand Down Expand Up @@ -149,7 +149,7 @@ jobs:
- run: |
BUILD_NUMBER=${{ github.sha }}
echo "BUILD_NUMBER=${BUILD_NUMBER::7}" >> "$GITHUB_ENV"
- uses: docker/setup-qemu-action@v3.3.0
- uses: docker/setup-qemu-action@v3.4.0
with:
platforms: arm64
- uses: docker/setup-buildx-action@v3
Expand Down
2 changes: 1 addition & 1 deletion migrations/1737473344288-create_wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class CreateWallets1737473344288 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "wallets" ("id" SERIAL NOT NULL, "address" character varying(42) NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "user_id" integer, CONSTRAINT "UQ_wallet_address" UNIQUE ("address"), CONSTRAINT "PK_wallet_id" PRIMARY KEY ("id"))`,
`CREATE TABLE "wallets" ("id" SERIAL NOT NULL, "address" character varying(42) NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "user_id" integer NOT NULL, CONSTRAINT "UQ_wallet_address" UNIQUE ("address"), CONSTRAINT "PK_wallet_id" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "wallets" ADD CONSTRAINT "FK_wallets_user_id" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
Expand Down
46 changes: 46 additions & 0 deletions migrations/1738679234042-user_organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class UserOrganization1738679234042 implements MigrationInterface {
name = 'UserOrganization1738679234042';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_organizations" ("id" SERIAL NOT NULL, "name" character varying(255), "role" integer NOT NULL, "status" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "userId" integer NOT NULL, "organizationId" integer NOT NULL, CONSTRAINT "UQ_user_organizations" UNIQUE ("userId", "organizationId"), CONSTRAINT "PK_UO_id" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "idx_UO_role_status" ON "user_organizations" ("role", "status")`,
);
await queryRunner.query(
`CREATE INDEX "idx_UO_name" ON "user_organizations" ("name")`,
);
await queryRunner.query(
`ALTER TABLE "user_organizations" ADD CONSTRAINT "FK_UO_user_id" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "user_organizations" ADD CONSTRAINT "FK_UO_organization_id" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(`
CREATE TRIGGER update_updated_at
BEFORE UPDATE
ON
user_organizations
FOR EACH ROW
EXECUTE PROCEDURE update_updated_at();
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP TRIGGER IF EXISTS update_updated_at ON user_organizations;`,
);
await queryRunner.query(
`ALTER TABLE "user_organizations" DROP CONSTRAINT "FK_UO_organization_id"`,
);
await queryRunner.query(
`ALTER TABLE "user_organizations" DROP CONSTRAINT "FK_UO_user_id"`,
);
await queryRunner.query(`DROP INDEX "public"."idx_UO_name"`);
await queryRunner.query(`DROP INDEX "public"."idx_UO_role_status"`);
await queryRunner.query(`DROP TABLE "user_organizations"`);
}
}
24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"format-check": "prettier --check .",
"generate-abis": "node ./scripts/generate-abis.js",
"postinstall": "yarn generate-abis",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/src/main",
"start": "node --env-file=.env node_modules/.bin/nest start",
"start:dev": "node --env-file=.env node_modules/.bin/nest start --watch",
"start:debug": "node --env-file=.env node_modules/.bin/nest start --debug --watch",
"start:prod": "node --env-file=.env dist/src/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint-check": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"test": "node --env-file-if-exists=./.env.test ./node_modules/.bin/jest",
Expand All @@ -30,14 +30,14 @@
"migration:revert": "yarn typeorm migration:revert"
},
"dependencies": {
"@aws-sdk/client-kms": "^3.741.0",
"@aws-sdk/client-s3": "^3.741.0",
"@aws-sdk/client-kms": "^3.744.0",
"@aws-sdk/client-s3": "^3.744.0",
"@fingerprintjs/fingerprintjs-pro-server-api": "^6.1.0",
"@nestjs/cli": "^10.4.9",
"@nestjs/common": "^11.0.7",
"@nestjs/common": "^11.0.9",
"@nestjs/config": "^4.0.0",
"@nestjs/core": "^11.0.7",
"@nestjs/platform-express": "^11.0.7",
"@nestjs/core": "^11.0.9",
"@nestjs/platform-express": "^11.0.9",
"@nestjs/schedule": "^5.0.1",
"@nestjs/serve-static": "^5.0.1",
"@nestjs/swagger": "^11.0.3",
Expand All @@ -63,23 +63,23 @@
"devDependencies": {
"@faker-js/faker": "^9.3.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.7",
"@nestjs/testing": "^11.0.9",
"@smithy/util-stream": "^4.0.2",
"@types/amqplib": "^0",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.0",
"@types/jest": "29.5.14",
"@types/jsonwebtoken": "^9",
"@types/lodash": "^4.17.14",
"@types/node": "^22.13.0",
"@types/node": "^22.13.1",
"@types/semver": "^7.5.8",
"@types/supertest": "^6.0.2",
"aws-sdk-client-mock": "^4.1.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"husky": "^9.1.7",
"jest": "29.7.0",
"prettier": "^3.4.2",
"prettier": "^3.5.0",
"source-map-support": "^0.5.20",
"supertest": "^7.0.0",
"ts-jest": "29.2.5",
Expand Down
3 changes: 0 additions & 3 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ import { OrganizationsModule } from '@/routes/organizations/organizations.module

@Module({})
export class AppModule implements NestModule {
// Important: values read via the config factory do not take the .env file
// into account. The .env file loading is done by the ConfigurationModule
// which is not available at this stage.
static register(configFactory = configuration): DynamicModule {
const {
auth: isAuthFeatureEnabled,
Expand Down
4 changes: 3 additions & 1 deletion src/config/entities/orm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { DataSource } from 'typeorm';
import configuration from '@/config/entities/configuration';
import { postgresConfig } from '@/config/entities/postgres.config';

const dbConfig = configuration().db;
export default new DataSource({
migrationsTableName: dbConfig.orm.migrationsTableName,
entities: ['dist/src/**/entities/*.entity.db.js'],
...postgresConfig({
...configuration().db.connection.postgres,
...dbConfig.connection.postgres,
type: 'postgres',
}),
});
14 changes: 14 additions & 0 deletions src/datasources/db/v2/entities/row.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod';

export type Row = z.infer<typeof RowSchema>;

/**
* Note: this is a base schema for all entities that are meant to be persisted to the database.
* The 'id' field is a primary key, and the 'created_at' and 'updated_at' fields are timestamps.
* These fields shouldn't be modified by the application, and should be managed by the database.
*/
export const RowSchema = z.object({
id: z.number().int(),
createdAt: z.date(),
updatedAt: z.date(),
});
31 changes: 26 additions & 5 deletions src/datasources/organizations/entities/organizations.entity.db.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
Entity,
Index,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import {
OrganizationStatus,
Organization as DomainOrganization,
} from '@/domain/organizations/entities/organization.entity';
import { UserOrganization } from '@/datasources/users/entities/user-organizations.entity.db';
import { databaseEnumTransformer } from '@/domain/common/utils/enum';

// @todo make organizations singular, The database table name should remain plural
@Entity('organizations')
export class Organizations implements DomainOrganization {
export class Organization implements DomainOrganization {
@PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_org_id' })
id!: number;

Expand All @@ -15,18 +24,30 @@ export class Organizations implements DomainOrganization {
@Index('idx_org_status')
@Column({
type: 'integer',
transformer: databaseEnumTransformer(OrganizationStatus),
})
status!: OrganizationStatus;
status!: keyof typeof OrganizationStatus;

@Column({
name: 'created_at',
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
created_at!: Date;
createdAt!: Date;

@Column({
name: 'updated_at',
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
updated_at!: Date;
updatedAt!: Date;

@OneToMany(
() => UserOrganization,
(userOrganization: UserOrganization) => userOrganization.organization,
{ cascade: ['update', 'insert'] },
)
userOrganizations!: Array<UserOrganization>;
}
82 changes: 82 additions & 0 deletions src/datasources/users/entities/user-organizations.entity.db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Organization } from '@/datasources/organizations/entities/organizations.entity.db';
import { User } from '@/datasources/users/entities/users.entity.db';
import { databaseEnumTransformer } from '@/domain/common/utils/enum';
import {
UserOrganizationRole,
UserOrganizationStatus,
} from '@/domain/users/entities/user-organization.entity';
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Unique,
} from 'typeorm';

@Entity('user_organizations')
@Unique('UQ_user_organizations', ['user', 'organization'])
@Index('idx_UO_name', ['name'])
@Index('idx_UO_role_status', ['role', 'status'])
export class UserOrganization {
@PrimaryGeneratedColumn({
primaryKeyConstraintName: 'PK_UO_id',
})
id!: number;

@ManyToOne(() => User, (user: User) => user.id, {
cascade: true,
nullable: false,
})
@JoinColumn({
foreignKeyConstraintName: 'FK_UO_user_id',
})
user!: User;

@ManyToOne(
() => Organization,
(organization: Organization) => organization.id,
{
cascade: true,
nullable: false,
},
)
@JoinColumn({
foreignKeyConstraintName: 'FK_UO_organization_id',
})
organization!: Organization;

@Column({ type: 'varchar', length: 255, nullable: true })
name!: string | null;

// Postgres enums are string therefore we use integer
@Column({
type: 'integer',
transformer: databaseEnumTransformer(UserOrganizationRole),
})
role!: keyof typeof UserOrganizationRole;

// Postgres enums are string therefore we use integer
@Column({
type: 'integer',
transformer: databaseEnumTransformer(UserOrganizationStatus),
})
status!: keyof typeof UserOrganizationStatus;

@Column({
name: 'created_at',
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
createdAt!: Date;

@Column({
name: 'updated_at',
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
updatedAt!: Date;
}
14 changes: 13 additions & 1 deletion src/datasources/users/entities/users.entity.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ import {
User as DomainUser,
} from '@/domain/users/entities/user.entity';
import { Wallet } from '@/datasources/wallets/entities/wallets.entity.db';
import { UserOrganization } from '@/datasources/users/entities/user-organizations.entity.db';
import { databaseEnumTransformer } from '@/domain/common/utils/enum';

@Entity('users')
export class User implements DomainUser {
@PrimaryGeneratedColumn({ primaryKeyConstraintName: 'PK_id' })
id!: number;

// Postgres enums are string therefore we use integer
@Index('idx_user_status')
@Column({
type: 'integer',
transformer: databaseEnumTransformer(UserStatus),
})
status!: UserStatus;
status!: keyof typeof UserStatus;

@OneToMany(() => Wallet, (wallet: Wallet) => wallet.id, {
onDelete: 'CASCADE',
Expand All @@ -30,12 +34,20 @@ export class User implements DomainUser {
@Column({
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
created_at!: Date;

@Column({
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
updated_at!: Date;

@OneToMany(
() => UserOrganization,
(userOrganization: UserOrganization) => userOrganization.user,
)
user_organizations!: Array<UserOrganization>;
}
2 changes: 2 additions & 0 deletions src/datasources/wallets/entities/wallets.entity.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ export class Wallet implements z.infer<typeof WalletSchema> {
@Column({
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
created_at!: Date;

@Column({
type: 'timestamp with time zone',
default: () => 'CURRENT_TIMESTAMP',
update: false,
})
updated_at!: Date;
}
22 changes: 22 additions & 0 deletions src/domain/common/utils/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { ValueTransformer } from 'typeorm';

export function getEnumKey<
T extends { [key: string]: number | string; [key: number]: string },
>(enumObj: T, value: number): keyof T {
const key = enumObj[value];
if (typeof key !== 'string') {
throw new Error(`Invalid enum value: ${value}`);
}
return key;
}

export const databaseEnumTransformer = <
T extends { [key: string]: number | string; [key: number]: string },
>(
enumObj: T,
): ValueTransformer => {
return {
to: (value: keyof typeof enumObj) => enumObj[value],
from: (value: number): keyof T => getEnumKey(enumObj, value),
};
};
Loading

0 comments on commit 5955044

Please sign in to comment.