From dd84b5fb4b714fc4d94691a7e9cee0921f36b7ef Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Sun, 10 May 2026 10:45:06 +0300 Subject: [PATCH 01/14] chore(auth-core): drizzle start --- packages/auth-core/drizzle.config.mts | 22 ++++++++++++++++++++++ packages/auth-core/package.json | 6 ++++-- pnpm-lock.yaml | 6 ++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 packages/auth-core/drizzle.config.mts diff --git a/packages/auth-core/drizzle.config.mts b/packages/auth-core/drizzle.config.mts new file mode 100644 index 00000000..39b8540c --- /dev/null +++ b/packages/auth-core/drizzle.config.mts @@ -0,0 +1,22 @@ +import { ConnectionConfig } from 'pg'; +import { defineConfig } from 'drizzle-kit'; +import { getConfig, initConfig } from './src/config.js'; + +import { createConnectionOptions } from './src/db/createConnection'; +import { ConnectionOptions } from 'node:tls'; + +await initConfig(); + +const dbOptions = createConnectionOptions(getConfig().get('db')) as Omit, 'password' | 'ssl'> & { + password: string; + ssl?: ConnectionOptions; +}; + +export default defineConfig({ + schema: ['./src/users/user.ts'], + out: './src/db/migrations-drizzle', + dialect: 'postgresql', + dbCredentials: dbOptions, + verbose: true, + strict: true, +}); diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index e0ae3d20..2bc28014 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -37,7 +37,8 @@ }, "dependencies": { "pg": "catalog:", - "typeorm": "catalog:" + "typeorm": "catalog:", + "drizzle-orm": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", @@ -47,7 +48,8 @@ "@types/node": "catalog:", "@types/pg": "catalog:", "ts-node": "^10.9.1", - "typescript": "catalog:" + "typescript": "catalog:", + "drizzle-kit": "catalog:" }, "peerDependencies": { "@map-colonies/js-logger": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 435c70c2..440125a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -986,6 +986,9 @@ importers: '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) + drizzle-orm: + specifier: 'catalog:' + version: 0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.20.0)(pg@8.20.0) pg: specifier: 'catalog:' version: 8.20.0 @@ -1011,6 +1014,9 @@ importers: '@types/pg': specifier: 'catalog:' version: 8.20.0 + drizzle-kit: + specifier: 'catalog:' + version: 0.31.10 ts-node: specifier: ^10.9.1 version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) From 5ddb8d8fb5a4030c72a2716cc1ef2c41833e6196 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 13 May 2026 13:25:49 +0300 Subject: [PATCH 02/14] chore(global): drizzle progress --- apps/auth-manager/package.json | 3 +- .../src/asset/DAL/assetRepository.ts | 8 +- apps/auth-manager/src/common/constants.ts | 13 +- apps/auth-manager/src/common/db/pagination.ts | 11 + apps/auth-manager/src/containerConfig.ts | 86 +-- .../src/domain/DAL/domainRepository.ts | 50 +- .../domain/controllers/domainController.ts | 2 +- .../src/domain/models/domainManager.ts | 14 +- .../tests/integration/asset/asset.spec.mts | 20 +- packages/auth-bundler/src/db.ts | 8 +- packages/auth-bundler/tests/db.spec.ts | 8 +- packages/auth-core/drizzle.config.mts | 16 +- packages/auth-core/package.json | 8 +- packages/auth-core/src/db/entities/asset.ts | 65 +- packages/auth-core/src/db/entities/bundle.ts | 88 ++- packages/auth-core/src/db/entities/client.ts | 88 ++- packages/auth-core/src/db/entities/common.ts | 7 + .../auth-core/src/db/entities/connection.ts | 99 ++- packages/auth-core/src/db/entities/domain.ts | 32 +- packages/auth-core/src/db/entities/index.ts | 1 + packages/auth-core/src/db/entities/key.ts | 49 +- packages/auth-core/src/db/index.ts | 2 - .../db/migrations/1679474009991-addDomain.ts | 16 - .../db/migrations/1679992858635-addClient.ts | 16 - .../src/db/migrations/1680069089971-addKey.ts | 18 - .../db/migrations/1680156507067-addAsset.ts | 20 - .../migrations/1680430616430-addConnection.ts | 16 - .../db/migrations/1681050416393-addBundle.ts | 16 - .../migrations/1745474706009-requiredKeys.ts | 15 - .../1749972920049-addOpaVersionToBundle.ts | 18 - .../migration.sql | 65 ++ .../snapshot.json | 651 ++++++++++++++++++ packages/auth-core/src/db/migrations/index.ts | 12 - packages/auth-core/src/db/types/interfaces.ts | 16 +- .../src/db/utils/createConnection.ts | 90 +-- pnpm-lock.yaml | 402 +++-------- pnpm-workspace.yaml | 4 +- 37 files changed, 1260 insertions(+), 793 deletions(-) create mode 100644 packages/auth-core/src/db/entities/common.ts delete mode 100644 packages/auth-core/src/db/migrations/1679474009991-addDomain.ts delete mode 100644 packages/auth-core/src/db/migrations/1679992858635-addClient.ts delete mode 100644 packages/auth-core/src/db/migrations/1680069089971-addKey.ts delete mode 100644 packages/auth-core/src/db/migrations/1680156507067-addAsset.ts delete mode 100644 packages/auth-core/src/db/migrations/1680430616430-addConnection.ts delete mode 100644 packages/auth-core/src/db/migrations/1681050416393-addBundle.ts delete mode 100644 packages/auth-core/src/db/migrations/1745474706009-requiredKeys.ts delete mode 100644 packages/auth-core/src/db/migrations/1749972920049-addOpaVersionToBundle.ts create mode 100644 packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql create mode 100644 packages/auth-core/src/db/migrations/20260513061159_curious_speedball/snapshot.json delete mode 100644 packages/auth-core/src/db/migrations/index.ts diff --git a/apps/auth-manager/package.json b/apps/auth-manager/package.json index 1b100954..41f9acaa 100644 --- a/apps/auth-manager/package.json +++ b/apps/auth-manager/package.json @@ -65,7 +65,7 @@ "prom-client": "catalog:", "reflect-metadata": "catalog:", "tsyringe": "catalog:", - "typeorm": "catalog:" + "drizzle-orm": "catalog:" }, "devDependencies": { "@map-colonies/openapi-helpers": "catalog:", @@ -84,7 +84,6 @@ "jest-extended": "catalog:", "jest-openapi": "catalog:", "supertest": "catalog:", - "ts-node": "^10.9.1", "type-fest": "^4.40.0", "typescript": "catalog:", "vitest": "catalog:", diff --git a/apps/auth-manager/src/asset/DAL/assetRepository.ts b/apps/auth-manager/src/asset/DAL/assetRepository.ts index cfa9da77..dec943e8 100644 --- a/apps/auth-manager/src/asset/DAL/assetRepository.ts +++ b/apps/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,9 +1,9 @@ -import { Asset } from '@map-colonies/auth-core'; +import { assetTable } from '@map-colonies/auth-core'; import type { FactoryFunction } from 'tsyringe'; import type { Repository } from 'typeorm'; import { DataSource } from 'typeorm'; -export type AssetRepository = Repository & { +export type AssetRepository = Repository & { getMaxVersionWithLock: (name: string) => Promise; getMaxVersion: (name: string) => Promise; }; @@ -11,13 +11,13 @@ export type AssetRepository = Repository & { export const assetRepositoryFactory: FactoryFunction = (container) => { const dataSource = container.resolve(DataSource); - return dataSource.getRepository(Asset).extend({ + return dataSource.getRepository(assetTable).extend({ async getMaxVersionWithLock(name: string): Promise { const result = await this.createQueryBuilder() .select('version') .where('name = :name') .andWhere((qb) => { - const subQuery = qb.subQuery().select('MAX(version)').from(Asset, 'asset').where('name = :name').getQuery(); + const subQuery = qb.subQuery().select('MAX(version)').from(assetTable, 'asset').where('name = :name').getQuery(); return 'Asset.version = ' + subQuery; }) diff --git a/apps/auth-manager/src/common/constants.ts b/apps/auth-manager/src/common/constants.ts index ec2c126f..96877ca5 100644 --- a/apps/auth-manager/src/common/constants.ts +++ b/apps/auth-manager/src/common/constants.ts @@ -16,11 +16,12 @@ export const SERVICES = { TRACER: Symbol('Tracer'), METRICS: Symbol('Metrics'), HEALTHCHECK: Symbol('Healthcheck'), - DOMAIN_REPOSITORY: Symbol('DOMAIN_REPO'), - CLIENT_REPOSITORY: Symbol('CLIENT_REPO'), - KEY_REPOSITORY: Symbol('KEY_REPO'), - ASSET_REPOSITORY: Symbol('ASSET_REPO'), - CONNECTION_REPOSITORY: Symbol('CONNECTION_REPO'), - BUNDLE_REPOSITORY: Symbol('BUNDLE_REPO'), + DRIZZLE: Symbol('Drizzle'), + // DOMAIN_REPOSITORY: Symbol('DOMAIN_REPO'), + // CLIENT_REPOSITORY: Symbol('CLIENT_REPO'), + // KEY_REPOSITORY: Symbol('KEY_REPO'), + // ASSET_REPOSITORY: Symbol('ASSET_REPO'), + // CONNECTION_REPOSITORY: Symbol('CONNECTION_REPO'), + // BUNDLE_REPOSITORY: Symbol('BUNDLE_REPO'), } satisfies Record; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/apps/auth-manager/src/common/db/pagination.ts b/apps/auth-manager/src/common/db/pagination.ts index 94ec83c5..fbf50540 100644 --- a/apps/auth-manager/src/common/db/pagination.ts +++ b/apps/auth-manager/src/common/db/pagination.ts @@ -1,3 +1,6 @@ +import { SQL } from 'drizzle-orm'; +import { PgColumn, PgSelect } from 'drizzle-orm/pg-core'; + export const DEFAULT_PAGE_SIZE = 10; export interface PaginationParams { page: number; @@ -16,3 +19,11 @@ export function paginationParamsToFindOptions(paginationParams?: PaginationParam skip: (page - 1) * pageSize, }; } + +export function withPagination(qb: T, orderByColumn: PgColumn | SQL | SQL.Aliased, params: PaginationParams): T { + const { page, pageSize } = params; + return qb + .orderBy(orderByColumn) + .limit(pageSize) + .offset((page - 1) * pageSize); +} diff --git a/apps/auth-manager/src/containerConfig.ts b/apps/auth-manager/src/containerConfig.ts index 3826307e..b411523a 100644 --- a/apps/auth-manager/src/containerConfig.ts +++ b/apps/auth-manager/src/containerConfig.ts @@ -3,9 +3,10 @@ import { trace } from '@opentelemetry/api'; import { instanceCachingFactory } from 'tsyringe'; import type { DependencyContainer } from 'tsyringe/dist/typings/types'; import { jsLogger } from '@map-colonies/js-logger'; -import { DataSource } from 'typeorm'; +// import { DataSource } from 'typeorm'; import type { HealthCheck } from '@godaddy/terminus'; -import { Bundle, initConnection } from '@map-colonies/auth-core'; +import { Pool } from 'pg'; +import { Bundle, createConnectionOptions, initConnection } from '@map-colonies/auth-core'; import { Registry } from 'prom-client'; import { DB_CONNECTION_TIMEOUT, SERVICES, SERVICE_NAME } from './common/constants'; import { domainRouterFactory, DOMAIN_ROUTER_SYMBOL } from './domain/routes/domainRouter'; @@ -20,12 +21,12 @@ import { assetRouterFactory, ASSET_ROUTER_SYMBOL } from './asset/routes/assetRou import { assetRepositoryFactory } from './asset/DAL/assetRepository'; import { connectionRepositoryFactory } from './connection/DAL/connectionRepository'; import { connectionRouterFactory, CONNECTION_ROUTER_SYMBOL } from './connection/routes/connectionRouter'; -import { domainRepositoryFactory } from './domain/DAL/domainRepository'; +// import { domainRepositoryFactory } from './domain/DAL/domainRepository'; import { bundleRouterFactory, BUNDLE_ROUTER_SYMBOL } from './bundle/routes/bundleRouter'; import { getConfig } from './common/config'; import { getTracing } from './common/tracing'; -const healthCheck = (connection: DataSource): HealthCheck => { +const healthCheck = (connection: Pool): HealthCheck => { return async (): Promise => { const check = connection.query('SELECT 1').then(() => { return; @@ -47,7 +48,14 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise const logger = await jsLogger({ ...loggerConfig, prettyPrint: loggerConfig.prettyPrint, mixin: getOtelMixin() }); const dataSourceOptions = configInstance.get('db'); - const connection = await initConnection(dataSourceOptions); + // const connection = await initConnection(dataSourceOptions); + + let pool: Pool; + try { + pool = await initConnection(dataSourceOptions); + } catch (error) { + throw new Error(`Failed to connect to the database`, { cause: error }); + } const tracer = trace.getTracer(SERVICE_NAME); const metricsRegistry = new Registry(); @@ -58,53 +66,53 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise { token: SERVICES.LOGGER, provider: { useValue: logger } }, { token: SERVICES.TRACER, provider: { useValue: tracer } }, { token: SERVICES.METRICS, provider: { useValue: metricsRegistry } }, - { token: DataSource, provider: { useValue: connection } }, + { token: Pool, provider: { useValue: pool } }, { token: SERVICES.HEALTHCHECK, provider: { useFactory: instanceCachingFactory((container) => { - const connection = container.resolve(DataSource); + const connection = container.resolve(Pool); return healthCheck(connection); }), }, }, - { - token: SERVICES.DOMAIN_REPOSITORY, - provider: { useFactory: instanceCachingFactory(domainRepositoryFactory) }, - }, - { token: DOMAIN_ROUTER_SYMBOL, provider: { useFactory: domainRouterFactory } }, - { - token: SERVICES.CLIENT_REPOSITORY, - provider: { useFactory: instanceCachingFactory(clientRepositoryFactory) }, - }, - { token: CLIENT_ROUTER_SYMBOL, provider: { useFactory: clientRouterFactory } }, - { - token: SERVICES.KEY_REPOSITORY, - provider: { useFactory: instanceCachingFactory(keyRepositoryFactory) }, - }, - { token: KEY_ROUTER_SYMBOL, provider: { useFactory: keyRouterFactory } }, - { - token: SERVICES.ASSET_REPOSITORY, - provider: { useFactory: instanceCachingFactory(assetRepositoryFactory) }, - }, - { token: ASSET_ROUTER_SYMBOL, provider: { useFactory: assetRouterFactory } }, - { - token: SERVICES.CONNECTION_REPOSITORY, - provider: { useFactory: instanceCachingFactory(connectionRepositoryFactory) }, - }, - { token: CONNECTION_ROUTER_SYMBOL, provider: { useFactory: connectionRouterFactory } }, - { - token: SERVICES.BUNDLE_REPOSITORY, - provider: { - useFactory: instanceCachingFactory((c) => c.resolve(DataSource).getRepository(Bundle)), - }, - }, + // { + // token: SERVICES.DOMAIN_REPOSITORY, + // provider: { useFactory: instanceCachingFactory(domainRepositoryFactory) }, + // }, + // { token: DOMAIN_ROUTER_SYMBOL, provider: { useFactory: domainRouterFactory } }, + // { + // token: SERVICES.CLIENT_REPOSITORY, + // provider: { useFactory: instanceCachingFactory(clientRepositoryFactory) }, + // }, + // { token: CLIENT_ROUTER_SYMBOL, provider: { useFactory: clientRouterFactory } }, + // { + // token: SERVICES.KEY_REPOSITORY, + // provider: { useFactory: instanceCachingFactory(keyRepositoryFactory) }, + // }, + // { token: KEY_ROUTER_SYMBOL, provider: { useFactory: keyRouterFactory } }, + // { + // token: SERVICES.ASSET_REPOSITORY, + // provider: { useFactory: instanceCachingFactory(assetRepositoryFactory) }, + // }, + // { token: ASSET_ROUTER_SYMBOL, provider: { useFactory: assetRouterFactory } }, + // { + // token: SERVICES.CONNECTION_REPOSITORY, + // provider: { useFactory: instanceCachingFactory(connectionRepositoryFactory) }, + // }, + // { token: CONNECTION_ROUTER_SYMBOL, provider: { useFactory: connectionRouterFactory } }, + // { + // token: SERVICES.BUNDLE_REPOSITORY, + // provider: { + // useFactory: instanceCachingFactory((c) => c.resolve(DataSource).getRepository(Bundle)), + // }, + // }, { token: BUNDLE_ROUTER_SYMBOL, provider: { useFactory: bundleRouterFactory } }, { token: 'onSignal', provider: { useValue: async (): Promise => { - await Promise.all([getTracing().stop(), connection.destroy()]); + await Promise.all([getTracing().stop(), pool.end()]); }, }, }, diff --git a/apps/auth-manager/src/domain/DAL/domainRepository.ts b/apps/auth-manager/src/domain/DAL/domainRepository.ts index 8e950938..3420db6f 100644 --- a/apps/auth-manager/src/domain/DAL/domainRepository.ts +++ b/apps/auth-manager/src/domain/DAL/domainRepository.ts @@ -1,26 +1,36 @@ import { Domain } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository } from 'typeorm'; +import { SERVICES } from '@src/common/constants'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { inject, Lifecycle, scoped, type FactoryFunction } from 'tsyringe'; +import type { Logger, Repository } from 'typeorm'; import { DataSource } from 'typeorm'; -export type DomainRepository = Repository & { - checkInputForNonExistingDomains: (domainNames: string[]) => Promise; -}; +// export type DomainRepository = Repository & { +// checkInputForNonExistingDomains: (domainNames: string[]) => Promise; +// }; -export const domainRepositoryFactory: FactoryFunction = (container) => { - const dataSource = container.resolve(DataSource); +// export const domainRepositoryFactory: FactoryFunction = (container) => { +// const dataSource = container.resolve(DataSource); - return dataSource.getRepository(Domain).extend({ - async checkInputForNonExistingDomains(domainNames: string[]): Promise { - // unnest is a postgresql only function on array datatype - // I wrote raw sql because typeorm doesn't think that using a function in FROM is a real thing and treats it like a table name - const res = (await this.manager.query( - ` - SELECT i.name FROM unnest($1::text[]) i(name) LEFT JOIN auth_manager.domain d ON i.name = d.name WHERE d.name is NULL`, - [domainNames] - )) as unknown as { name: string }[]; +// return dataSource.getRepository(Domain).extend({ +// async checkInputForNonExistingDomains(domainNames: string[]): Promise { +// // unnest is a postgresql only function on array datatype +// // I wrote raw sql because typeorm doesn't think that using a function in FROM is a real thing and treats it like a table name +// const res = (await this.manager.query( +// ` +// SELECT i.name FROM unnest($1::text[]) i(name) LEFT JOIN auth_manager.domain d ON i.name = d.name WHERE d.name is NULL`, +// [domainNames] +// )) as unknown as { name: string }[]; - return res.map((domain) => domain.name); - }, - }); -}; +// return res.map((domain) => domain.name); +// }, +// }); +// }; + +@scoped(Lifecycle.ContainerScoped) +export class ConfigRepository { + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(SERVICES.DRIZZLE) private readonly drizzle: NodePgDatabase + ) {} +} diff --git a/apps/auth-manager/src/domain/controllers/domainController.ts b/apps/auth-manager/src/domain/controllers/domainController.ts index e432fde1..3e4d658d 100644 --- a/apps/auth-manager/src/domain/controllers/domainController.ts +++ b/apps/auth-manager/src/domain/controllers/domainController.ts @@ -1,7 +1,7 @@ import { HttpError } from '@map-colonies/error-express-handler'; import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; -import { Domain } from '@map-colonies/auth-core'; +import type { Domain } from '@map-colonies/auth-core'; import type { TypedRequestHandlers } from 'auth-openapi'; import { sortOptionParser } from '@src/common/db/sort'; import { DEFAULT_PAGE_SIZE } from '@src/common/db/pagination'; diff --git a/apps/auth-manager/src/domain/models/domainManager.ts b/apps/auth-manager/src/domain/models/domainManager.ts index 67dc7f45..dbfdc812 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -1,18 +1,20 @@ import { type Logger } from '@map-colonies/js-logger'; -import { Domain, IDomain } from '@map-colonies/auth-core'; +import { Domain, domainTable, IDomain } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -import { FindManyOptions } from 'typeorm'; +// import { FindManyOptions } from 'typeorm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { where } from 'drizzle-orm'; import { PaginationParams, paginationParamsToFindOptions } from '@src/common/db/pagination'; import { SortOptions } from '@src/common/db/sort'; import { SERVICES } from '@common/constants'; -import { type DomainRepository } from '../DAL/domainRepository'; +// import { type DomainRepository } from '../DAL/domainRepository'; import { DomainAlreadyExistsError } from './errors'; @injectable() export class DomainManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.DOMAIN_REPOSITORY) private readonly domainRepository: DomainRepository + @inject(SERVICES.DRIZZLE) private readonly drizzle: NodePgDatabase ) {} public async getDomains(paginationParams?: PaginationParams, sortParams?: SortOptions): Promise<[IDomain[], number]> { @@ -28,8 +30,8 @@ export class DomainManager { if (sortParams !== undefined) { findOptions.order = sortParams; } - - return this.domainRepository.findAndCount(findOptions); + return this.drizzle.select().from(domainTable); + // return this.domainRepository.findAndCount(findOptions); } public async createDomain(domain: IDomain): Promise { diff --git a/apps/auth-manager/tests/integration/asset/asset.spec.mts b/apps/auth-manager/tests/integration/asset/asset.spec.mts index 521569e9..bcb8ee8d 100644 --- a/apps/auth-manager/tests/integration/asset/asset.spec.mts +++ b/apps/auth-manager/tests/integration/asset/asset.spec.mts @@ -7,7 +7,7 @@ import type { DependencyContainer } from 'tsyringe'; import 'jest-openapi'; import { DataSource } from 'typeorm'; import type { IAsset } from '@map-colonies/auth-core'; -import { Asset, AssetType, Environment } from '@map-colonies/auth-core'; +import { assetTable, AssetType, Environment } from '@map-colonies/auth-core'; import { faker } from '@faker-js/faker'; import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; @@ -52,7 +52,7 @@ describe('client', function () { ]; const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(assets); + await connection.getRepository(assetTable).save(assets); const res = await requestSender.getAssets(); @@ -71,7 +71,7 @@ describe('client', function () { ]; const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(assets); + await connection.getRepository(assetTable).save(assets); const res = await requestSender.getAssets({ queryParams: { environment: ['prod'] } }); @@ -89,7 +89,7 @@ describe('client', function () { ]; const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(assets); + await connection.getRepository(assetTable).save(assets); const res = await requestSender.getAssets({ queryParams: { type: AssetType.DATA } }); @@ -115,7 +115,7 @@ describe('client', function () { it('should return 200 status code and the updated asset', async function () { const asset = getFakeAsset(); const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(asset); + await connection.getRepository(assetTable).save(asset); delete asset.createdAt; @@ -133,7 +133,7 @@ describe('client', function () { const assets: IAsset[] = [asset, { ...asset, version: 2 }]; const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(assets); + await connection.getRepository(assetTable).save(assets); const res = await requestSender.getAsset({ pathParams: { assetName: asset.name } }); @@ -147,7 +147,7 @@ describe('client', function () { it('should return 200 status code and the requested asset', async function () { const asset = getFakeAsset(); const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(asset); + await connection.getRepository(assetTable).save(asset); const res = await requestSender.getVersionedAsset({ pathParams: { assetName: asset.name, version: asset.version } }); @@ -163,7 +163,7 @@ describe('client', function () { const assets: IAsset[] = [baseAsset, { ...baseAsset, version: 2 }, { ...baseAsset, version: 3 }]; const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(assets); + await connection.getRepository(assetTable).save(assets); const expectedAsset = assets.find((a) => a.version === 3); @@ -177,7 +177,7 @@ describe('client', function () { it('should return 200 status code and the only asset when there is only one version', async function () { const asset = getFakeAsset(); const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save(asset); + await connection.getRepository(assetTable).save(asset); const res = await requestSender.getLatestAsset({ pathParams: { assetName: asset.name } }); @@ -201,7 +201,7 @@ describe('client', function () { it("should return 409 if the request version doesn't match the DB version", async function () { const asset = getFakeAsset(); const connection = depContainer.resolve(DataSource); - await connection.getRepository(Asset).save({ ...asset }); + await connection.getRepository(assetTable).save({ ...asset }); const res = await requestSender.upsertAsset({ requestBody: { ...asset, version: 2 } }); diff --git a/packages/auth-bundler/src/db.ts b/packages/auth-bundler/src/db.ts index 53a89991..daac0a88 100644 --- a/packages/auth-bundler/src/db.ts +++ b/packages/auth-bundler/src/db.ts @@ -1,5 +1,5 @@ import type { DataSource, Repository } from 'typeorm'; -import { Asset, Bundle, Connection, type Environments, Key } from '@map-colonies/auth-core'; +import { assetTable, Bundle, Connection, type Environments, Key } from '@map-colonies/auth-core'; import type { BundleContent, BundleContentVersions } from './types'; import { extractNameAndVersion } from './util'; import { logger } from './logger'; @@ -10,7 +10,7 @@ import { getVersionCommand } from './opa'; * This class handles all the database interactions required to creating a bundle. */ export class BundleDatabase { - private readonly assetRepository: Repository; + private readonly assetRepository: Repository; private readonly keyRepository: Repository; private readonly connectionRepository: Repository; private readonly bundleRepository: Repository; @@ -26,7 +26,7 @@ export class BundleDatabase { if (!dataSource.isInitialized) { throw new ConnectionNotInitializedError('DB connection it not initialized'); } - this.assetRepository = dataSource.getRepository(Asset); + this.assetRepository = dataSource.getRepository(assetTable); this.keyRepository = dataSource.getRepository(Key); this.connectionRepository = dataSource.getRepository(Connection); this.bundleRepository = dataSource.getRepository(Bundle); @@ -77,7 +77,7 @@ export class BundleDatabase { logger?.debug('fetching bundle from the db'); const assets = this.dataSource - .getRepository(Asset) + .getRepository(assetTable) .createQueryBuilder() .whereInIds(extractNameAndVersion(versions.assets)) .andWhere(':env = ANY(environment)', { env: versions.environment }) diff --git a/packages/auth-bundler/tests/db.spec.ts b/packages/auth-bundler/tests/db.spec.ts index 8da4c92b..25ef91b3 100644 --- a/packages/auth-bundler/tests/db.spec.ts +++ b/packages/auth-bundler/tests/db.spec.ts @@ -1,6 +1,6 @@ /// -import { Asset, Bundle, Connection, Environment, Key, createConnectionOptions } from '@map-colonies/auth-core'; +import { assetTable, Bundle, Connection, Environment, Key, createConnectionOptions } from '@map-colonies/auth-core'; import { DataSource } from 'typeorm'; import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest'; import { ConnectionNotInitializedError } from '@src/index'; @@ -39,7 +39,7 @@ describe('db.ts', function () { const [privateKey, publicKey] = getMockKeys(); await dataSource.getRepository(Key).save({ environment: Environment.PRODUCTION, version: 1, privateKey, publicKey }); - await dataSource.getRepository(Asset).save([ + await dataSource.getRepository(assetTable).save([ { ...asset, version: 1 }, { ...asset, version: 2 }, ]); @@ -110,7 +110,7 @@ describe('db.ts', function () { }); it('should return the latest version of the asset even if there is a newer version in the database with a different environment', async function () { - await dataSource.getRepository(Asset).save([ + await dataSource.getRepository(assetTable).save([ { ...asset, name: 'xd', environment: [Environment.STAGE, Environment.NP], version: 1 }, { ...asset, name: 'xd', environment: [Environment.STAGE], version: 2 }, ]); @@ -133,7 +133,7 @@ describe('db.ts', function () { keyVersion: 1, }); - expect(assets).toSatisfyAll((a) => a.environment.includes(Environment.PRODUCTION)); + expect(assets).toSatisfyAll((a) => a.environment.includes(Environment.PRODUCTION)); expect(assets[0]).toHaveProperty('version', 1); }); }); diff --git a/packages/auth-core/drizzle.config.mts b/packages/auth-core/drizzle.config.mts index 39b8540c..8cacb79c 100644 --- a/packages/auth-core/drizzle.config.mts +++ b/packages/auth-core/drizzle.config.mts @@ -2,21 +2,25 @@ import { ConnectionConfig } from 'pg'; import { defineConfig } from 'drizzle-kit'; import { getConfig, initConfig } from './src/config.js'; -import { createConnectionOptions } from './src/db/createConnection'; +import { createConnectionOptions } from './src/db/utils/createConnection.js'; import { ConnectionOptions } from 'node:tls'; await initConfig(); -const dbOptions = createConnectionOptions(getConfig().get('db')) as Omit, 'password' | 'ssl'> & { +const config = getConfig().getAll(); + +const dbOptions = createConnectionOptions(config) as Omit, 'password' | 'ssl'> & { password: string; ssl?: ConnectionOptions; }; export default defineConfig({ - schema: ['./src/users/user.ts'], - out: './src/db/migrations-drizzle', + schema: ['./src/db/entities/index.ts'], + out: './src/db/migrations', dialect: 'postgresql', - dbCredentials: dbOptions, + schemaFilter: ['auth_manager', 'public'], + dbCredentials: { ...dbOptions, user: dbOptions.user, ssl: false }, verbose: true, - strict: true, + migrations: { schema: config.schema }, + strict: false, }); diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index 2bc28014..8ca8f2fe 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -19,9 +19,8 @@ }, "scripts": { "typeorm": "node node_modules/typeorm/cli.js -d ./dataSource.mjs", - "migration:create": "npm run typeorm migration:generate --", + "migration:create": "drizzle-kit generate --config drizzle.config.mts", "migration:run": "npm run typeorm migration:run -- ", - "migration:revert": "npm run typeorm migration:revert -- ", "lint": "eslint .", "lint:fix": "eslint --fix .", "prebuild": "pnpm run clean", @@ -37,12 +36,11 @@ }, "dependencies": { "pg": "catalog:", - "typeorm": "catalog:", - "drizzle-orm": "catalog:" + "drizzle-orm": "catalog:", + "@map-colonies/schemas": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", - "@map-colonies/schemas": "catalog:", "@map-colonies/tsconfig": "catalog:", "@map-colonies/eslint-config": "catalog:", "@types/node": "catalog:", diff --git a/packages/auth-core/src/db/entities/asset.ts b/packages/auth-core/src/db/entities/asset.ts index 43fb1da4..ec41a171 100644 --- a/packages/auth-core/src/db/entities/asset.ts +++ b/packages/auth-core/src/db/entities/asset.ts @@ -1,43 +1,22 @@ -import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; -import type { AssetTypes, Environments, IAsset } from '../../model'; -import { Environment, AssetType } from '../../model'; - -/** - * The typeorm implementation of the IAsset interface. - */ -@Entity() -export class Asset implements IAsset { - @PrimaryColumn({ type: 'varchar' }) - public name!: string; - - @PrimaryColumn({ type: 'integer' }) - public version!: number; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - public createdAt!: Date; - - @Column({ - type: 'bytea', - transformer: { - from(value: Buffer): string { - return value.toString('base64'); - }, - to(value: string): Buffer { - return Buffer.from(value, 'base64'); - }, - }, - }) - public value!: string; - - @Column({ type: 'varchar' }) - public uri!: string; - - @Column({ type: 'enum', enum: AssetType }) - public type!: AssetTypes; - - @Column({ type: 'enum', enum: Environment, array: true, enumName: 'environment_enum' }) - public environment!: Environments[]; - - @Column({ type: 'boolean', name: 'is_template' }) - public isTemplate!: boolean; -} +import { boolean, bytea, integer, primaryKey, varchar } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; + +export const assetTypeEnum = authManagerSchema.enum('asset_type_enum', ['TEST', 'TEST_DATA', 'POLICY', 'DATA']); + +export const assetTable = authManagerSchema.table( + 'asset', + { + name: varchar().notNull(), + version: integer().notNull(), + createdAt: createdAtColumn, + value: bytea().notNull(), + uri: varchar().notNull(), + type: assetTypeEnum().notNull(), + environment: environmentEnum().array().notNull(), + isTemplate: boolean().notNull(), + }, + (table) => [primaryKey({ columns: [table.name, table.version], name: 'PK_c3670311f777dc6ab9965408f97' })] +); + +export type Asset = typeof assetTable.$inferSelect; +export type NewAsset = typeof assetTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/bundle.ts b/packages/auth-core/src/db/entities/bundle.ts index 1f31ac48..e175919f 100644 --- a/packages/auth-core/src/db/entities/bundle.ts +++ b/packages/auth-core/src/db/entities/bundle.ts @@ -1,35 +1,53 @@ -import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; -import { Environment, type Environments, type IBundle } from '../../model'; - -/** - * The typeorm implementation of the IBundle interface. - */ -@Entity() -export class Bundle implements IBundle { - @PrimaryColumn({ generated: 'identity', generatedIdentity: 'ALWAYS', insert: false, type: 'integer' }) - public id!: number; - - @Column({ type: 'text', nullable: true }) - public hash?: string; - - @Column({ type: 'enum', enum: Environment, enumName: 'environment_enum' }) - public environment!: Environments; - - @Column({ type: 'jsonb', nullable: true }) - public metadata?: Record; - - @Column({ type: 'jsonb', nullable: true }) - public assets?: { name: string; version: number }[]; - - @Column({ type: 'jsonb', nullable: true }) - public connections?: { name: string; version: number }[]; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - public createdAt?: Date; - - @Column({ name: 'key_version', type: 'integer', nullable: true }) - public keyVersion?: number; - - @Column({ name: 'opa_version', type: 'text', nullable: false }) - public opaVersion!: string; -} +// import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; +// import { Environment, type Environments, type IBundle } from '../../model'; + +import { integer, jsonb, text, timestamp } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; + +// /** +// * The typeorm implementation of the IBundle interface. +// */ +// @Entity() +// export class Bundle implements IBundle { +// @PrimaryColumn({ generated: 'identity', generatedIdentity: 'ALWAYS', insert: false, type: 'integer' }) +// public id!: number; + +// @Column({ type: 'text', nullable: true }) +// public hash?: string; + +// @Column({ type: 'enum', enum: Environment, enumName: 'environment_enum' }) +// public environment!: Environments; + +// @Column({ type: 'jsonb', nullable: true }) +// public metadata?: Record; + +// @Column({ type: 'jsonb', nullable: true }) +// public assets?: { name: string; version: number }[]; + +// @Column({ type: 'jsonb', nullable: true }) +// public connections?: { name: string; version: number }[]; + +// @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) +// public createdAt?: Date; + +// @Column({ name: 'key_version', type: 'integer', nullable: true }) +// public keyVersion?: number; + +// @Column({ name: 'opa_version', type: 'text', nullable: false }) +// public opaVersion!: string; +// } + +export const bundleTable = authManagerSchema.table('bundle', { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + hash: text(), + environment: environmentEnum().notNull(), + metadata: jsonb(), + assets: jsonb(), + connections: jsonb(), + createdAt: createdAtColumn, + keyVersion: integer(), + opaVersion: text().notNull(), +}); + +export type Bundle = typeof bundleTable.$inferSelect; +export type NewBundle = typeof bundleTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/client.ts b/packages/auth-core/src/db/entities/client.ts index 5f80ea0b..e49067fc 100644 --- a/packages/auth-core/src/db/entities/client.ts +++ b/packages/auth-core/src/db/entities/client.ts @@ -1,35 +1,53 @@ -import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; -import type { IClient, PointOfContact } from '../../model'; - -/** - * The typeorm implementation of the IClient interface. - */ -@Entity() -export class Client implements IClient { - @PrimaryColumn({ name: 'name', type: 'text', unique: true }) - public name!: string; - - @Column({ type: 'text', name: 'heb_name' }) - public hebName!: string; - - @Column({ type: 'text', nullable: true }) - public description?: string; - - @Column({ type: 'text', nullable: true }) - public branch?: string; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - public createdAt?: Date; - - @UpdateDateColumn({ name: 'update_at', type: 'timestamptz' }) - public updatedAt?: Date; - - @Column({ type: 'json', name: 'tech_point_of_contact', nullable: true }) - public techPointOfContact?: PointOfContact; - - @Column({ type: 'json', name: 'product_point_of_contact', nullable: true }) - public productPointOfContact?: PointOfContact; - - @Column({ type: 'text', array: true, nullable: true }) - public tags?: string[] | undefined; -} +// import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +// import type { IClient, PointOfContact } from '../../model'; + +import { json, text, timestamp } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn } from './common'; + +// /** +// * The typeorm implementation of the IClient interface. +// */ +// @Entity() +// export class Client implements IClient { +// @PrimaryColumn({ name: 'name', type: 'text', unique: true }) +// public name!: string; + +// @Column({ type: 'text', name: 'heb_name' }) +// public hebName!: string; + +// @Column({ type: 'text', nullable: true }) +// public description?: string; + +// @Column({ type: 'text', nullable: true }) +// public branch?: string; + +// @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) +// public createdAt?: Date; + +// @UpdateDateColumn({ name: 'update_at', type: 'timestamptz' }) +// public updatedAt?: Date; + +// @Column({ type: 'json', name: 'tech_point_of_contact', nullable: true }) +// public techPointOfContact?: PointOfContact; + +// @Column({ type: 'json', name: 'product_point_of_contact', nullable: true }) +// public productPointOfContact?: PointOfContact; + +// @Column({ type: 'text', array: true, nullable: true }) +// public tags?: string[] | undefined; +// } + +export const clientTable = authManagerSchema.table('client', { + name: text().primaryKey(), + hebName: text().notNull(), + description: text(), + branch: text(), + createdAt: createdAtColumn, + updateAt: timestamp({ withTimezone: true }).defaultNow().notNull(), + techPointOfContact: json('tech_point_of_contact'), + productPointOfContact: json('product_point_of_contact'), + tags: text().array(), +}); + +export type Client = typeof clientTable.$inferSelect; +export type NewClient = typeof clientTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/common.ts b/packages/auth-core/src/db/entities/common.ts new file mode 100644 index 00000000..8cd1ff2a --- /dev/null +++ b/packages/auth-core/src/db/entities/common.ts @@ -0,0 +1,7 @@ +import * as d from 'drizzle-orm/pg-core'; +import { timestamp } from 'drizzle-orm/pg-core'; + +export const authManagerSchema = d.snakeCase.schema('auth_manager'); +export const environmentEnum = authManagerSchema.enum('environment_enum', ['np', 'stage', 'prod']); + +export const createdAtColumn = timestamp({ withTimezone: true }).defaultNow().notNull(); diff --git a/packages/auth-core/src/db/entities/connection.ts b/packages/auth-core/src/db/entities/connection.ts index 3c186e92..4e7a30b3 100644 --- a/packages/auth-core/src/db/entities/connection.ts +++ b/packages/auth-core/src/db/entities/connection.ts @@ -1,38 +1,61 @@ -import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; -import { Environment, type IConnection, type Environments } from '../../model'; - -/** - * The typeorm implementation of the IConnection interface. - */ -@Entity() -export class Connection implements IConnection { - @PrimaryColumn({ type: 'varchar' }) - public name!: string; - - @PrimaryColumn({ type: 'integer' }) - public version!: number; - - @PrimaryColumn({ type: 'enum', enum: Environment, enumName: 'environment_enum' }) - public environment!: Environments; - - @Column({ type: 'boolean' }) - public enabled!: boolean; - - @Column({ type: 'text' }) - public token!: string; - - @Column({ type: 'boolean', name: 'allow_no_browser' }) - public allowNoBrowserConnection!: boolean; - - @Column({ type: 'boolean', name: 'allow_no_origin' }) - public allowNoOriginConnection!: boolean; - - @Column({ type: 'text', array: true }) - public domains!: string[]; - - @Column({ type: 'text', array: true }) - public origins!: string[]; - - @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) - public createdAt!: Date; -} +// import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; +// import { Environment, type IConnection, type Environments } from '../../model'; + +import { boolean, integer, primaryKey, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; + +// /** +// * The typeorm implementation of the IConnection interface. +// */ +// @Entity() +// export class Connection implements IConnection { +// @PrimaryColumn({ type: 'varchar' }) +// public name!: string; + +// @PrimaryColumn({ type: 'integer' }) +// public version!: number; + +// @PrimaryColumn({ type: 'enum', enum: Environment, enumName: 'environment_enum' }) +// public environment!: Environments; + +// @Column({ type: 'boolean' }) +// public enabled!: boolean; + +// @Column({ type: 'text' }) +// public token!: string; + +// @Column({ type: 'boolean', name: 'allow_no_browser' }) +// public allowNoBrowserConnection!: boolean; + +// @Column({ type: 'boolean', name: 'allow_no_origin' }) +// public allowNoOriginConnection!: boolean; + +// @Column({ type: 'text', array: true }) +// public domains!: string[]; + +// @Column({ type: 'text', array: true }) +// public origins!: string[]; + +// @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) +// public createdAt!: Date; +// } + +export const connectionTable = authManagerSchema.table( + 'connection', + { + name: varchar().notNull(), + version: integer().notNull(), + environment: environmentEnum().notNull(), + enabled: boolean().notNull(), + token: text().notNull(), + allowNoBrowser: boolean().notNull(), + allowNoOrigin: boolean().notNull(), + domains: text().array().notNull(), + origins: text().array().notNull(), + createdAt: createdAtColumn, + }, + (table) => [primaryKey({ columns: [table.name, table.version, table.environment], name: 'PK_4c3be048a366c9ce9277bac4c38' })] +); + +export type Connection = typeof connectionTable.$inferSelect; +export type NewConnection = typeof connectionTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/domain.ts b/packages/auth-core/src/db/entities/domain.ts index 02e4c77d..4c6ec85f 100644 --- a/packages/auth-core/src/db/entities/domain.ts +++ b/packages/auth-core/src/db/entities/domain.ts @@ -1,11 +1,21 @@ -import { Entity, PrimaryColumn } from 'typeorm'; -import type { IDomain } from '../../model'; - -/** - * The typeorm implementation of the IDomain interface. - */ -@Entity() -export class Domain implements IDomain { - @PrimaryColumn({ name: 'name', type: 'text', unique: true }) - public name!: string; -} +// import { Entity, PrimaryColumn } from 'typeorm'; +// import type { IDomain } from '../../model'; + +import { text } from 'drizzle-orm/pg-core'; +import { authManagerSchema } from './common'; + +// /** +// * The typeorm implementation of the IDomain interface. +// */ +// @Entity() +// export class Domain implements IDomain { +// @PrimaryColumn({ name: 'name', type: 'text', unique: true }) +// public name!: string; +// } + +export const domainTable = authManagerSchema.table('domain', { + name: text().primaryKey(), +}); + +export type Domain = typeof domainTable.$inferSelect; +export type NewDomain = typeof domainTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/index.ts b/packages/auth-core/src/db/entities/index.ts index 037b453a..bf185f22 100644 --- a/packages/auth-core/src/db/entities/index.ts +++ b/packages/auth-core/src/db/entities/index.ts @@ -4,3 +4,4 @@ export * from './client'; export * from './connection'; export * from './domain'; export * from './key'; +export * from './common'; diff --git a/packages/auth-core/src/db/entities/key.ts b/packages/auth-core/src/db/entities/key.ts index 88f3a7cf..89e01883 100644 --- a/packages/auth-core/src/db/entities/key.ts +++ b/packages/auth-core/src/db/entities/key.ts @@ -1,20 +1,37 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; -import { Environment, type Environments, type IKey, type JWKPrivateKey, type JWKPublicKey } from '../../model'; +// import { Column, Entity, PrimaryColumn } from 'typeorm'; +// import { Environment, type Environments, type IKey, type JWKPrivateKey, type JWKPublicKey } from '../../model'; -/** - * The typeorm implementation of the IKey interface. - */ -@Entity() -export class Key implements IKey { - @PrimaryColumn({ type: 'enum', enum: Environment, unique: true, enumName: 'environment_enum' }) - public environment!: Environments; +import { integer, jsonb, primaryKey } from 'drizzle-orm/pg-core'; +import { authManagerSchema, environmentEnum } from './common'; - @PrimaryColumn({ type: 'integer' }) - public version!: number; +// /** +// * The typeorm implementation of the IKey interface. +// */ +// @Entity() +// export class Key implements IKey { +// @PrimaryColumn({ type: 'enum', enum: Environment, unique: true, enumName: 'environment_enum' }) +// public environment!: Environments; - @Column({ type: 'jsonb', name: 'private_key' }) - public privateKey!: JWKPrivateKey; +// @PrimaryColumn({ type: 'integer' }) +// public version!: number; - @Column({ type: 'jsonb', name: 'public_key' }) - public publicKey!: JWKPublicKey; -} +// @Column({ type: 'jsonb', name: 'private_key' }) +// public privateKey!: JWKPrivateKey; + +// @Column({ type: 'jsonb', name: 'public_key' }) +// public publicKey!: JWKPublicKey; +// } + +export const keyTable = authManagerSchema.table( + 'key', + { + environment: environmentEnum().notNull(), + version: integer().notNull(), + privateKey: jsonb().notNull(), + publicKey: jsonb().notNull(), + }, + (table) => [primaryKey({ columns: [table.environment, table.version], name: 'PK_ddf3d991c46b66651794ee56d58' })] +); + +export type Key = typeof keyTable.$inferSelect; +export type NewKey = typeof keyTable.$inferInsert; diff --git a/packages/auth-core/src/db/index.ts b/packages/auth-core/src/db/index.ts index 95967516..cdc18dcd 100644 --- a/packages/auth-core/src/db/index.ts +++ b/packages/auth-core/src/db/index.ts @@ -1,4 +1,2 @@ export * from './entities'; -export * from './migrations'; -export * from './types'; export * from './utils'; diff --git a/packages/auth-core/src/db/migrations/1679474009991-addDomain.ts b/packages/auth-core/src/db/migrations/1679474009991-addDomain.ts deleted file mode 100644 index 0001a48d..00000000 --- a/packages/auth-core/src/db/migrations/1679474009991-addDomain.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addDomain1679474009991 implements MigrationInterface { - name = 'addDomain1679474009991'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "auth_manager"."domain" ("name" text NOT NULL, CONSTRAINT "PK_26a07113f90df161f919c7d5a65" PRIMARY KEY ("name"))` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "auth_manager"."domain"`); - } -} diff --git a/packages/auth-core/src/db/migrations/1679992858635-addClient.ts b/packages/auth-core/src/db/migrations/1679992858635-addClient.ts deleted file mode 100644 index 014d5c28..00000000 --- a/packages/auth-core/src/db/migrations/1679992858635-addClient.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addClient1679992858635 implements MigrationInterface { - name = 'addClient1679992858635'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "auth_manager"."client" ("name" text NOT NULL, "heb_name" text NOT NULL, "description" text, "branch" text, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "update_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "tech_point_of_contact" json, "product_point_of_contact" json, "tags" text array, CONSTRAINT "PK_480f88a019346eae487a0cd7f0c" PRIMARY KEY ("name"))` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "auth_manager"."client"`); - } -} diff --git a/packages/auth-core/src/db/migrations/1680069089971-addKey.ts b/packages/auth-core/src/db/migrations/1680069089971-addKey.ts deleted file mode 100644 index c4b85fdf..00000000 --- a/packages/auth-core/src/db/migrations/1680069089971-addKey.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addKey1680069089971 implements MigrationInterface { - name = 'addKey1680069089971'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TYPE "auth_manager"."key_environment_enum" AS ENUM('np', 'stage', 'prod')`); - await queryRunner.query( - `CREATE TABLE "auth_manager"."key" ("environment" "auth_manager"."key_environment_enum" NOT NULL, "version" integer NOT NULL, "private_key" jsonb, "public_key" jsonb, CONSTRAINT "PK_ddf3d991c46b66651794ee56d58" PRIMARY KEY ("environment", "version"))` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "auth_manager"."key"`); - await queryRunner.query(`DROP TYPE "auth_manager"."key_environment_enum"`); - } -} diff --git a/packages/auth-core/src/db/migrations/1680156507067-addAsset.ts b/packages/auth-core/src/db/migrations/1680156507067-addAsset.ts deleted file mode 100644 index 9276ebe2..00000000 --- a/packages/auth-core/src/db/migrations/1680156507067-addAsset.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addAsset1680156507067 implements MigrationInterface { - name = 'addAsset1680156507067'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`CREATE TYPE "auth_manager"."asset_type_enum" AS ENUM('TEST', 'TEST_DATA', 'POLICY', 'DATA')`); - await queryRunner.query(`ALTER TYPE "auth_manager"."key_environment_enum" RENAME TO "environment_enum"`); - await queryRunner.query( - `CREATE TABLE "auth_manager"."asset" ("name" character varying NOT NULL, "version" integer NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "value" bytea NOT NULL, "uri" character varying NOT NULL, "type" "auth_manager"."asset_type_enum" NOT NULL, "environment" "auth_manager"."environment_enum" array NOT NULL, "is_template" boolean NOT NULL, CONSTRAINT "PK_c3670311f777dc6ab9965408f97" PRIMARY KEY ("name", "version"))` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "auth_manager"."asset"`); - await queryRunner.query(`ALTER TYPE "auth_manager"."environment_enum" RENAME TO "key_environment_enum"`); - await queryRunner.query(`DROP TYPE "auth_manager"."asset_type_enum"`); - } -} diff --git a/packages/auth-core/src/db/migrations/1680430616430-addConnection.ts b/packages/auth-core/src/db/migrations/1680430616430-addConnection.ts deleted file mode 100644 index caf1636c..00000000 --- a/packages/auth-core/src/db/migrations/1680430616430-addConnection.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addConnection1680430616430 implements MigrationInterface { - name = 'addConnection1680430616430'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "auth_manager"."connection" ("name" character varying NOT NULL, "version" integer NOT NULL, "environment" "auth_manager"."environment_enum" NOT NULL, "enabled" boolean NOT NULL, "token" text NOT NULL, "allow_no_browser" boolean NOT NULL, "allow_no_origin" boolean NOT NULL, "domains" text array NOT NULL, "origins" text array NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_4c3be048a366c9ce9277bac4c38" PRIMARY KEY ("name", "version", "environment"))` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "auth_manager"."connection"`); - } -} diff --git a/packages/auth-core/src/db/migrations/1681050416393-addBundle.ts b/packages/auth-core/src/db/migrations/1681050416393-addBundle.ts deleted file mode 100644 index 8513ee53..00000000 --- a/packages/auth-core/src/db/migrations/1681050416393-addBundle.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* eslint-disable */ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class addBundle1681050416393 implements MigrationInterface { - name = 'addBundle1681050416393'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "auth_manager"."bundle" ("id" integer GENERATED ALWAYS AS IDENTITY NOT NULL, "hash" text, "environment" "auth_manager"."environment_enum" NOT NULL, "metadata" jsonb, "assets" jsonb, "connections" jsonb, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "key_version" integer, CONSTRAINT "PK_637e3f87e837d6532109c198dea" PRIMARY KEY ("id"))` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE "auth_manager"."bundle"`); - } -} diff --git a/packages/auth-core/src/db/migrations/1745474706009-requiredKeys.ts b/packages/auth-core/src/db/migrations/1745474706009-requiredKeys.ts deleted file mode 100644 index f62857fe..00000000 --- a/packages/auth-core/src/db/migrations/1745474706009-requiredKeys.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class RequiredKeys1745474706009 implements MigrationInterface { - public name = 'RequiredKeys1745474706009'; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "auth_manager"."key" ALTER COLUMN "private_key" SET NOT NULL`); - await queryRunner.query(`ALTER TABLE "auth_manager"."key" ALTER COLUMN "public_key" SET NOT NULL`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "auth_manager"."key" ALTER COLUMN "public_key" DROP NOT NULL`); - await queryRunner.query(`ALTER TABLE "auth_manager"."key" ALTER COLUMN "private_key" DROP NOT NULL`); - } -} diff --git a/packages/auth-core/src/db/migrations/1749972920049-addOpaVersionToBundle.ts b/packages/auth-core/src/db/migrations/1749972920049-addOpaVersionToBundle.ts deleted file mode 100644 index d65069d4..00000000 --- a/packages/auth-core/src/db/migrations/1749972920049-addOpaVersionToBundle.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddOpaVersionToBundle1749972920049 implements MigrationInterface { - public name = 'AddOpaVersionToBundle1749972920049'; - - public async up(queryRunner: QueryRunner): Promise { - // Add the column as nullable first - await queryRunner.query(`ALTER TABLE "auth_manager"."bundle" ADD "opa_version" text`); - // Set default value for existing bundles - await queryRunner.query(`UPDATE "auth_manager"."bundle" SET "opa_version" = '0.52.0' WHERE "opa_version" IS NULL`); - // Make the column NOT NULL - await queryRunner.query(`ALTER TABLE "auth_manager"."bundle" ALTER COLUMN "opa_version" SET NOT NULL`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "auth_manager"."bundle" DROP COLUMN "opa_version"`); - } -} diff --git a/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql b/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql new file mode 100644 index 00000000..ab413f43 --- /dev/null +++ b/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql @@ -0,0 +1,65 @@ +CREATE SCHEMA "auth_manager"; +--> statement-breakpoint +CREATE TYPE "auth_manager"."asset_type_enum" AS ENUM('TEST', 'TEST_DATA', 'POLICY', 'DATA');--> statement-breakpoint +CREATE TYPE "auth_manager"."environment_enum" AS ENUM('np', 'stage', 'prod');--> statement-breakpoint +CREATE TABLE "auth_manager"."asset" ( + "name" varchar, + "version" integer, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "value" bytea NOT NULL, + "uri" varchar NOT NULL, + "type" "auth_manager"."asset_type_enum" NOT NULL, + "environment" "auth_manager"."environment_enum"[] NOT NULL, + "is_template" boolean NOT NULL, + CONSTRAINT "PK_c3670311f777dc6ab9965408f97" PRIMARY KEY("name","version") +); +--> statement-breakpoint +CREATE TABLE "auth_manager"."bundle" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "auth_manager"."bundle_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "hash" text, + "environment" "auth_manager"."environment_enum" NOT NULL, + "metadata" jsonb, + "assets" jsonb, + "connections" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "key_version" integer, + "opa_version" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "auth_manager"."client" ( + "name" text PRIMARY KEY, + "heb_name" text NOT NULL, + "description" text, + "branch" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "update_at" timestamp with time zone DEFAULT now() NOT NULL, + "tech_point_of_contact" json, + "product_point_of_contact" json, + "tags" text[] +); +--> statement-breakpoint +CREATE TABLE "auth_manager"."connection" ( + "name" varchar, + "version" integer, + "environment" "auth_manager"."environment_enum", + "enabled" boolean NOT NULL, + "token" text NOT NULL, + "allow_no_browser" boolean NOT NULL, + "allow_no_origin" boolean NOT NULL, + "domains" text[] NOT NULL, + "origins" text[] NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "PK_4c3be048a366c9ce9277bac4c38" PRIMARY KEY("name","version","environment") +); +--> statement-breakpoint +CREATE TABLE "auth_manager"."domain" ( + "name" text PRIMARY KEY +); +--> statement-breakpoint +CREATE TABLE "auth_manager"."key" ( + "environment" "auth_manager"."environment_enum", + "version" integer, + "private_key" jsonb NOT NULL, + "public_key" jsonb NOT NULL, + CONSTRAINT "PK_ddf3d991c46b66651794ee56d58" PRIMARY KEY("environment","version") +); diff --git a/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/snapshot.json b/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/snapshot.json new file mode 100644 index 00000000..3960d72c --- /dev/null +++ b/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/snapshot.json @@ -0,0 +1,651 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "bf848b15-704f-44e1-9541-6173dc6afa43", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "ddl": [ + { + "name": "auth_manager", + "entityType": "schemas" + }, + { + "values": ["TEST", "TEST_DATA", "POLICY", "DATA"], + "name": "asset_type_enum", + "entityType": "enums", + "schema": "auth_manager" + }, + { + "values": ["np", "stage", "prod"], + "name": "environment_enum", + "entityType": "enums", + "schema": "auth_manager" + }, + { + "isRlsEnabled": false, + "name": "asset", + "entityType": "tables", + "schema": "auth_manager" + }, + { + "isRlsEnabled": false, + "name": "bundle", + "entityType": "tables", + "schema": "auth_manager" + }, + { + "isRlsEnabled": false, + "name": "client", + "entityType": "tables", + "schema": "auth_manager" + }, + { + "isRlsEnabled": false, + "name": "connection", + "entityType": "tables", + "schema": "auth_manager" + }, + { + "isRlsEnabled": false, + "name": "domain", + "entityType": "tables", + "schema": "auth_manager" + }, + { + "isRlsEnabled": false, + "name": "key", + "entityType": "tables", + "schema": "auth_manager" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "bytea", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "uri", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "asset_type_enum", + "typeSchema": "auth_manager", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "type", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "environment_enum", + "typeSchema": "auth_manager", + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "is_template", + "entityType": "columns", + "schema": "auth_manager", + "table": "asset" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": { + "type": "always", + "name": "bundle_id_seq", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": 1, + "cycle": false + }, + "name": "id", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hash", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "environment_enum", + "typeSchema": "auth_manager", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "assets", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "connections", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key_version", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "opa_version", + "entityType": "columns", + "schema": "auth_manager", + "table": "bundle" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "heb_name", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "branch", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "update_at", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tech_point_of_contact", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "json", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "product_point_of_contact", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "tags", + "entityType": "columns", + "schema": "auth_manager", + "table": "client" + }, + { + "type": "varchar", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "environment_enum", + "typeSchema": "auth_manager", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "enabled", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "allow_no_browser", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "allow_no_origin", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "domains", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 1, + "default": null, + "generated": null, + "identity": null, + "name": "origins", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "auth_manager", + "table": "connection" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "auth_manager", + "table": "domain" + }, + { + "type": "environment_enum", + "typeSchema": "auth_manager", + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment", + "entityType": "columns", + "schema": "auth_manager", + "table": "key" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "version", + "entityType": "columns", + "schema": "auth_manager", + "table": "key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "private_key", + "entityType": "columns", + "schema": "auth_manager", + "table": "key" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "public_key", + "entityType": "columns", + "schema": "auth_manager", + "table": "key" + }, + { + "columns": ["name", "version"], + "nameExplicit": true, + "name": "PK_c3670311f777dc6ab9965408f97", + "entityType": "pks", + "schema": "auth_manager", + "table": "asset" + }, + { + "columns": ["name", "version", "environment"], + "nameExplicit": true, + "name": "PK_4c3be048a366c9ce9277bac4c38", + "entityType": "pks", + "schema": "auth_manager", + "table": "connection" + }, + { + "columns": ["environment", "version"], + "nameExplicit": true, + "name": "PK_ddf3d991c46b66651794ee56d58", + "entityType": "pks", + "schema": "auth_manager", + "table": "key" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "bundle_pkey", + "schema": "auth_manager", + "table": "bundle", + "entityType": "pks" + }, + { + "columns": ["name"], + "nameExplicit": false, + "name": "client_pkey", + "schema": "auth_manager", + "table": "client", + "entityType": "pks" + }, + { + "columns": ["name"], + "nameExplicit": false, + "name": "domain_pkey", + "schema": "auth_manager", + "table": "domain", + "entityType": "pks" + } + ], + "renames": [] +} diff --git a/packages/auth-core/src/db/migrations/index.ts b/packages/auth-core/src/db/migrations/index.ts deleted file mode 100644 index 35a2e2ac..00000000 --- a/packages/auth-core/src/db/migrations/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ -import { readdirSync } from 'node:fs'; -import path from 'node:path'; - -/** - * A chronological sorted list of all the database migrations to create the latest authentication schema. - */ -export const migrations: Function[] = readdirSync(__dirname) - .filter((file) => /^\d[\da-zA-Z-]+\.(js|ts)$/.test(file)) - .sort() - .map((file) => Object.values(require(path.join(__dirname, file)))[0]) - .filter((func) => func !== undefined); diff --git a/packages/auth-core/src/db/types/interfaces.ts b/packages/auth-core/src/db/types/interfaces.ts index 6c707815..85831779 100644 --- a/packages/auth-core/src/db/types/interfaces.ts +++ b/packages/auth-core/src/db/types/interfaces.ts @@ -1,8 +1,8 @@ -import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -import type { commonDbFullV1Type } from '@map-colonies/schemas'; -/** - * An object describing all the necessary configuration to authenticate to a postgresql database. - * It is an extension of the {@link https://typeorm.io/data-source-options#postgres--cockroachdb-data-source-options | PostgresConnectionOptions} - * @property ssl include if Should database connection be authenticated using SSL certificates and if true so provide the paths for the SSL certificates and key. - */ -export type DbConfig = Pick & PostgresConnectionOptions; +// import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; +// import type { commonDbFullV1Type } from '@map-colonies/schemas'; +// /** +// * An object describing all the necessary configuration to authenticate to a postgresql database. +// * It is an extension of the {@link https://typeorm.io/data-source-options#postgres--cockroachdb-data-source-options | PostgresConnectionOptions} +// * @property ssl include if Should database connection be authenticated using SSL certificates and if true so provide the paths for the SSL certificates and key. +// */ +// export type DbConfig = Pick & PostgresConnectionOptions; diff --git a/packages/auth-core/src/db/utils/createConnection.ts b/packages/auth-core/src/db/utils/createConnection.ts index f8a68c6c..63f385ee 100644 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ b/packages/auth-core/src/db/utils/createConnection.ts @@ -1,45 +1,53 @@ import { hostname } from 'node:os'; -import { readFileSync } from 'node:fs'; -import type { TlsOptions } from 'node:tls'; -import { DataSource, type DataSourceOptions } from 'typeorm'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { Pool, type PoolConfig } from 'pg'; import type { commonDbFullV1Type } from '@map-colonies/schemas'; -import { migrations } from '../migrations'; -import { Asset, Bundle, Client, Connection, Domain, Key } from '../entities'; - -/** - * A helper function that creates the typeorm DataSource options to use for creating a new DataSource. - * Handles SSL and registration of all required entities and migrations. - * @param dbConfig The typeorm postgres configuration with added SSL options. - * @returns Options object ready to use with typeorm. - */ -export const createConnectionOptions = (dbConfig: commonDbFullV1Type): DataSourceOptions => { - let ssl: TlsOptions | undefined = undefined; - - const { ssl: inputSsl, ...dataSourceOptions } = dbConfig; - - if (inputSsl.enabled) { - ssl = { key: readFileSync(inputSsl.key), cert: readFileSync(inputSsl.cert), ca: readFileSync(inputSsl.ca) }; +import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { migrate } from 'drizzle-orm/node-postgres/migrator'; + +export function createConnectionOptions(dbOptions: commonDbFullV1Type): PoolConfig { + const { ssl } = dbOptions; + const dbConfig: PoolConfig = structuredClone(dbOptions); + dbConfig.application_name = `${hostname()}-${process.env.NODE_ENV ?? 'unknown_env'}`; + dbConfig.user = dbOptions.username; + if (ssl.enabled) { + dbConfig.password = undefined; + dbConfig.ssl = { key: readFileSync(ssl.key), cert: readFileSync(ssl.cert), ca: readFileSync(ssl.ca) }; + } + return dbConfig; +} + +export async function initConnection(dbConfig: commonDbFullV1Type): Promise { + const pool = new Pool(createConnectionOptions(dbConfig)); + await pool.query('SELECT NOW()'); + return pool; +} + +export function createDrizzle(pool: Pool): ReturnType { + return drizzle({ client: pool }); +} + +export async function runMigrations(drizzle: NodePgDatabase): Promise { + const optionalFolders = ['./db/migrations', './src/db/migrations', './migrations']; + let migrationsFolder = null; + + for (const folder of optionalFolders) { + if (!existsSync(folder)) { + continue; + } + const folderContent = readdirSync(folder, { withFileTypes: true }); + + const hasMigrations = folderContent.some((item) => item.isDirectory() && existsSync(path.join(folder, item.name, 'migration.sql'))); + if (hasMigrations) { + migrationsFolder = folder; + break; + } + } + + if (migrationsFolder === null) { + throw new Error('No migrations folder found'); } - return { - type: 'postgres', - entities: [Asset, Bundle, Client, Connection, Domain, Key], - migrations, - migrationsTableName: 'custom_migration_table', - applicationName: `${hostname()}-${process.env.NODE_ENV ?? 'unknown_env'}`, - ssl, - ...dataSourceOptions, - }; -}; - -/** - * Helper function to handle both the configuration and initialization of a typeORM datasource. - * Uses {@link createConnectionOptions} to handle the configuration. - * @param dbConfig The typeorm postgres configuration with added SSL options. - * @returns Ready to use typeorm DataSource. - */ -export const initConnection = async (dbConfig: commonDbFullV1Type): Promise => { - const dataSource = new DataSource(createConnectionOptions(dbConfig)); - await dataSource.initialize(); - return dataSource; -}; + await migrate(drizzle, { migrationsFolder: migrationsFolder, migrationsSchema: 'auth_manager' }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 440125a5..3b4bd778 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,11 +100,11 @@ catalogs: specifier: ^4.1.0 version: 4.1.0 drizzle-kit: - specifier: ^0.31.10 - version: 0.31.10 + specifier: 1.0.0-rc.2 + version: 1.0.0-rc.2 drizzle-orm: - specifier: ^0.45.2 - version: 0.45.2 + specifier: 1.0.0-rc.2 + version: 1.0.0-rc.2 eslint: specifier: ^9.39.2 version: 9.39.4 @@ -290,7 +290,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/express': specifier: 'catalog:' version: 4.17.25 @@ -308,7 +308,7 @@ importers: version: 4.1.4(vitest@4.1.4) jest-extended: specifier: 'catalog:' - version: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) + version: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) test-utils: specifier: workspace:^ version: link:../../packages/test-utils @@ -378,6 +378,9 @@ importers: date-fns: specifier: 'catalog:' version: 4.1.0 + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) express: specifier: 'catalog:' version: 4.22.1 @@ -402,9 +405,6 @@ importers: tsyringe: specifier: 'catalog:' version: 4.10.0 - typeorm: - specifier: 'catalog:' - version: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)) devDependencies: '@faker-js/faker': specifier: 'catalog:' @@ -420,7 +420,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/body-parser': specifier: 'catalog:' version: 1.19.6 @@ -453,7 +453,7 @@ importers: version: 4.1.4(vitest@4.1.4) jest-extended: specifier: 'catalog:' - version: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) + version: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) jest-openapi: specifier: 'catalog:' version: 0.14.2 @@ -463,9 +463,6 @@ importers: test-utils: specifier: workspace:^ version: link:../../packages/test-utils - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) type-fest: specifier: ^4.40.0 version: 4.41.0 @@ -785,7 +782,7 @@ importers: version: 4.1.0 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.20.0)(pg@8.20.0) + version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) express: specifier: 'catalog:' version: 4.22.1 @@ -849,7 +846,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/body-parser': specifier: 'catalog:' version: 1.19.6 @@ -888,7 +885,7 @@ importers: version: 7.0.3 drizzle-kit: specifier: 'catalog:' - version: 0.31.10 + version: 1.0.0-rc.2 jest-openapi: specifier: 'catalog:' version: 0.14.2 @@ -952,7 +949,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/node': specifier: 'catalog:' version: 24.12.0 @@ -967,7 +964,7 @@ importers: version: 4.1.4(vitest@4.1.4) jest-extended: specifier: 'catalog:' - version: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) + version: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) test-utils: specifier: workspace:^ version: link:../test-utils @@ -986,15 +983,15 @@ importers: '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) + '@map-colonies/schemas': + specifier: 'catalog:' + version: 1.21.0 drizzle-orm: specifier: 'catalog:' - version: 0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.20.0)(pg@8.20.0) + version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) pg: specifier: 'catalog:' version: 8.20.0 - typeorm: - specifier: 'catalog:' - version: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)) devDependencies: '@map-colonies/config': specifier: 'catalog:' @@ -1002,9 +999,6 @@ importers: '@map-colonies/eslint-config': specifier: 'catalog:' version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) - '@map-colonies/schemas': - specifier: 'catalog:' - version: 1.21.0 '@map-colonies/tsconfig': specifier: 'catalog:' version: 2.0.0 @@ -1016,7 +1010,7 @@ importers: version: 8.20.0 drizzle-kit: specifier: 'catalog:' - version: 0.31.10 + version: 1.0.0-rc.2 ts-node: specifier: ^10.9.1 version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) @@ -1756,8 +1750,8 @@ packages: '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} - '@drizzle-team/brocli@0.10.2': - resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@drizzle-team/brocli@0.11.0': + resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==} '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} @@ -1777,14 +1771,6 @@ packages: '@emotion/unitless@0.10.0': resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1797,12 +1783,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1815,12 +1795,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -1833,12 +1807,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -1851,12 +1819,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -1869,12 +1831,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -1887,12 +1843,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -1905,12 +1855,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -1923,12 +1867,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -1941,12 +1879,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -1959,12 +1891,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -1977,12 +1903,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -1995,12 +1915,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -2013,12 +1927,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -2031,12 +1939,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -2049,12 +1951,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -2067,12 +1963,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -2097,12 +1987,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -2127,12 +2011,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -2157,12 +2035,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -2175,12 +2047,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -2193,12 +2059,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -2211,12 +2071,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -2464,6 +2318,10 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@js-temporal/polyfill@0.5.1': + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} @@ -5897,15 +5755,16 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - drizzle-kit@0.31.10: - resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + drizzle-kit@1.0.0-rc.2: + resolution: {integrity: sha512-TRxUmj1wDA2QCt3GvuhfamvIa66wJ7+MzSxBMKkpRtYScjHTumT9BE+x6daSzuEacSrPEuUH5/cW1uo5RkoPIg==} hasBin: true - drizzle-orm@0.45.2: - resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + drizzle-orm@1.0.0-rc.2: + resolution: {integrity: sha512-UXYDkbplF5wX0hwxll+80QhEwUvAJLBu+tAK/d4fna18kLE6VuliAzufF/ieDEIJeSnLRYgtmsXD6x1Xuy1kIg==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' + '@effect/sql-pg': '>=4.0.0-beta.58 || >=4.0.0' '@electric-sql/pglite': '>=0.2.0' '@libsql/client': '>=0.10.0' '@libsql/client-wasm': '>=0.10.0' @@ -5913,31 +5772,40 @@ packages: '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 '@planetscale/database': '>=1.13' - '@prisma/client': '*' + '@sinclair/typebox': '>=0.34.8' + '@sqlitecloud/drivers': '>=1.0.653' '@tidbcloud/serverless': '*' + '@tursodatabase/database': '>=0.2.1' + '@tursodatabase/database-common': '>=0.2.1' + '@tursodatabase/database-wasm': '>=0.2.1' '@types/better-sqlite3': '*' + '@types/mssql': ^9.1.4 '@types/pg': '*' '@types/sql.js': '*' '@upstash/redis': '>=1.34.7' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' - better-sqlite3: '>=7' + arktype: '>=2.0.0' + better-sqlite3: '>=9.3.0' bun-types: '*' + effect: '>=4.0.0-beta.58 || >=4.0.0' expo-sqlite: '>=14.0.0' - gel: '>=2' - knex: '*' - kysely: '*' + mssql: ^11.0.1 mysql2: '>=2' pg: '>=8' postgres: '>=3' - prisma: '*' sql.js: '>=1' sqlite3: '>=5' + typebox: '>=1.0.0' + valibot: '>=1.0.0-beta.7' + zod: ^3.25.0 || ^4.0.0 peerDependenciesMeta: '@aws-sdk/client-rds-data': optional: true '@cloudflare/workers-types': optional: true + '@effect/sql-pg': + optional: true '@electric-sql/pglite': optional: true '@libsql/client': @@ -5952,12 +5820,22 @@ packages: optional: true '@planetscale/database': optional: true - '@prisma/client': + '@sinclair/typebox': + optional: true + '@sqlitecloud/drivers': optional: true '@tidbcloud/serverless': optional: true + '@tursodatabase/database': + optional: true + '@tursodatabase/database-common': + optional: true + '@tursodatabase/database-wasm': + optional: true '@types/better-sqlite3': optional: true + '@types/mssql': + optional: true '@types/pg': optional: true '@types/sql.js': @@ -5968,17 +5846,17 @@ packages: optional: true '@xata.io/client': optional: true + arktype: + optional: true better-sqlite3: optional: true bun-types: optional: true - expo-sqlite: - optional: true - gel: + effect: optional: true - knex: + expo-sqlite: optional: true - kysely: + mssql: optional: true mysql2: optional: true @@ -5986,12 +5864,16 @@ packages: optional: true postgres: optional: true - prisma: - optional: true sql.js: optional: true sqlite3: optional: true + typebox: + optional: true + valibot: + optional: true + zod: + optional: true dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -6081,11 +5963,6 @@ packages: es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -7186,6 +7063,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + jsep@1.4.0: resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} engines: {node: '>= 10.16.0'} @@ -9405,10 +9285,12 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -9704,9 +9586,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -10814,7 +10693,7 @@ snapshots: '@date-fns/tz@1.4.1': {} - '@drizzle-team/brocli@0.10.2': {} + '@drizzle-team/brocli@0.11.0': {} '@emnapi/core@1.9.1': dependencies: @@ -10840,160 +10719,102 @@ snapshots: '@emotion/unitless@0.10.0': {} - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.13.7 - '@esbuild/aix-ppc64@0.25.12': optional: true '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true @@ -11006,9 +10827,6 @@ snapshots: '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true @@ -11021,9 +10839,6 @@ snapshots: '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true @@ -11036,36 +10851,24 @@ snapshots: '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -11436,6 +11239,10 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@js-temporal/polyfill@0.5.1': + dependencies: + jsbi: 4.3.2 + '@jsdevtools/ono@7.1.3': {} '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': @@ -11612,11 +11419,11 @@ snapshots: '@map-colonies/tsconfig@2.0.0': {} - '@map-colonies/vitest-utils@0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4)': + '@map-colonies/vitest-utils@0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4)': dependencies: vitest: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: - jest-extended: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) + jest-extended: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) jest-openapi: 0.14.2 '@monaco-editor/loader@1.7.0': @@ -15309,18 +15116,21 @@ snapshots: dotenv@16.6.1: {} - drizzle-kit@0.31.10: + drizzle-kit@1.0.0-rc.2: dependencies: - '@drizzle-team/brocli': 0.10.2 - '@esbuild-kit/esm-loader': 2.6.5 + '@drizzle-team/brocli': 0.11.0 + '@js-temporal/polyfill': 0.5.1 esbuild: 0.25.12 - tsx: 4.21.0 + get-tsconfig: 4.13.7 + jiti: 2.6.1 - drizzle-orm@0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.20.0)(pg@8.20.0): + drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3): optionalDependencies: '@opentelemetry/api': 1.9.0 + '@sinclair/typebox': 0.34.48 '@types/pg': 8.20.0 pg: 8.20.0 + zod: 4.4.3 dunder-proto@1.0.1: dependencies: @@ -15453,31 +15263,6 @@ snapshots: es6-promise@3.3.1: {} - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -16739,7 +16524,7 @@ snapshots: jest-util: 29.7.0 optional: true - jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3): + jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3): dependencies: jest-diff: 30.3.0 typescript: 5.9.3 @@ -17015,6 +16800,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbi@4.3.2: {} + jsep@1.4.0: {} jsesc@3.1.0: {} @@ -17084,7 +16871,7 @@ snapshots: strip-json-comments: 5.0.3 unbash: 2.2.0 yaml: 2.8.3 - zod: 4.3.6 + zod: 4.4.3 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -19165,6 +18952,7 @@ snapshots: get-tsconfig: 4.13.7 optionalDependencies: fsevents: 2.3.3 + optional: true tsyringe@4.10.0: dependencies: @@ -19668,6 +19456,4 @@ snapshots: zod@3.25.76: {} - zod@4.3.6: {} - zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d38bc78a..d02eeca2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,7 +34,7 @@ catalog: compression: '1.8.1' 'date-fns': '^4.1.0' 'body-parser': '^2.2.2' - 'drizzle-orm': '^0.45.2' + 'drizzle-orm': '1.0.0-rc.2' 'express-openapi-validator': '^5.6.2' http-status-codes: '^2.3.0' openapi-fetch: '0.17.0' @@ -48,7 +48,7 @@ catalog: '@types/body-parser': '1.19.6' '@types/supertest': '^7.2.0' 'cross-env': '^7.0.3' - 'drizzle-kit': '^0.31.10' + 'drizzle-kit': '1.0.0-rc.2' 'jest-openapi': '^0.14.2' 'nock': '^14.0.14' 'supertest': '^7.1.0' From 74597eacfee2c41cd5fd7823a2846ae2a5c443ff Mon Sep 17 00:00:00 2001 From: CptSchnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 13 May 2026 20:54:09 +0300 Subject: [PATCH 03/14] chore(global): more progress --- apps/auth-manager/src/common/db/pagination.ts | 1 + .../src/domain/models/domainManager.ts | 6 +++--- .../auth-core/src/db/entities/connection.ts | 4 ++-- packages/auth-core/src/db/entities/index.ts | 18 ++++++++++++++++++ .../auth-core/src/db/utils/createConnection.ts | 7 +++++-- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/auth-manager/src/common/db/pagination.ts b/apps/auth-manager/src/common/db/pagination.ts index fbf50540..63d01036 100644 --- a/apps/auth-manager/src/common/db/pagination.ts +++ b/apps/auth-manager/src/common/db/pagination.ts @@ -20,6 +20,7 @@ export function paginationParamsToFindOptions(paginationParams?: PaginationParam }; } +// TODO - move to generic package export function withPagination(qb: T, orderByColumn: PgColumn | SQL | SQL.Aliased, params: PaginationParams): T { const { page, pageSize } = params; return qb diff --git a/apps/auth-manager/src/domain/models/domainManager.ts b/apps/auth-manager/src/domain/models/domainManager.ts index dbfdc812..fe6856b8 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -1,5 +1,5 @@ import { type Logger } from '@map-colonies/js-logger'; -import { Domain, domainTable, IDomain } from '@map-colonies/auth-core'; +import { Domain, domainTable, Drizzle, IDomain } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; // import { FindManyOptions } from 'typeorm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; @@ -14,7 +14,7 @@ import { DomainAlreadyExistsError } from './errors'; export class DomainManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.DRIZZLE) private readonly drizzle: NodePgDatabase + @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} public async getDomains(paginationParams?: PaginationParams, sortParams?: SortOptions): Promise<[IDomain[], number]> { @@ -30,7 +30,7 @@ export class DomainManager { if (sortParams !== undefined) { findOptions.order = sortParams; } - return this.drizzle.select().from(domainTable); + return this.drizzle.query.domain.findMany({ extras: {} }); // return this.domainRepository.findAndCount(findOptions); } diff --git a/packages/auth-core/src/db/entities/connection.ts b/packages/auth-core/src/db/entities/connection.ts index 4e7a30b3..4955492c 100644 --- a/packages/auth-core/src/db/entities/connection.ts +++ b/packages/auth-core/src/db/entities/connection.ts @@ -48,8 +48,8 @@ export const connectionTable = authManagerSchema.table( environment: environmentEnum().notNull(), enabled: boolean().notNull(), token: text().notNull(), - allowNoBrowser: boolean().notNull(), - allowNoOrigin: boolean().notNull(), + allowNoBrowserConnection: boolean('allow_no_browser').notNull(), + allowNoOriginConnection: boolean('allow_no_origin').notNull(), domains: text().array().notNull(), origins: text().array().notNull(), createdAt: createdAtColumn, diff --git a/packages/auth-core/src/db/entities/index.ts b/packages/auth-core/src/db/entities/index.ts index bf185f22..2a9b0504 100644 --- a/packages/auth-core/src/db/entities/index.ts +++ b/packages/auth-core/src/db/entities/index.ts @@ -1,3 +1,21 @@ +import { defineRelations } from 'drizzle-orm'; + +import { assetTable } from './asset'; +import { bundleTable } from './bundle'; +import { clientTable } from './client'; +import { connectionTable } from './connection'; +import { domainTable } from './domain'; +import { keyTable } from './key'; + +export const relations = defineRelations({ + asset: assetTable, + bundle: bundleTable, + client: clientTable, + connection: connectionTable, + domain: domainTable, + key: keyTable, +}); + export * from './asset'; export * from './bundle'; export * from './client'; diff --git a/packages/auth-core/src/db/utils/createConnection.ts b/packages/auth-core/src/db/utils/createConnection.ts index 63f385ee..bb7d210a 100644 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ b/packages/auth-core/src/db/utils/createConnection.ts @@ -5,6 +5,7 @@ import { Pool, type PoolConfig } from 'pg'; import type { commonDbFullV1Type } from '@map-colonies/schemas'; import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { relations } from '../entities'; export function createConnectionOptions(dbOptions: commonDbFullV1Type): PoolConfig { const { ssl } = dbOptions; @@ -24,8 +25,10 @@ export async function initConnection(dbConfig: commonDbFullV1Type): Promise { - return drizzle({ client: pool }); +export type Drizzle = ReturnType>; + +export function createDrizzle(pool: Pool): Drizzle { + return drizzle({ client: pool, relations }); } export async function runMigrations(drizzle: NodePgDatabase): Promise { From 40722a6261f78375bd85442a74f0b3cb9d165640 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Thu, 14 May 2026 16:13:33 +0300 Subject: [PATCH 04/14] chore(global): progress --- .../src/asset/controllers/assetController.ts | 6 +- .../src/asset/models/assetManager.ts | 15 ++-- apps/auth-manager/src/common/db/pagination.ts | 10 +-- .../src/domain/models/domainManager.ts | 33 +++++---- packages/auth-core/package.json | 2 +- packages/auth-core/src/db/entities/bundle.ts | 6 +- packages/auth-core/src/db/entities/client.ts | 5 +- packages/auth-core/src/db/entities/key.ts | 5 +- .../src/db/utils/createConnection.ts | 1 + packages/auth-core/src/model/asset.ts | 74 +++++++++---------- packages/auth-core/src/model/bundle.ts | 42 +++++------ packages/auth-core/src/model/client.ts | 46 ++++++------ packages/auth-core/src/model/common.ts | 20 ++--- packages/auth-core/src/model/connection.ts | 52 ++++++------- packages/auth-core/src/model/domain.ts | 6 +- packages/auth-core/src/model/key.ts | 24 +++--- packages/test-utils/package.json | 6 +- packages/test-utils/src/drizzle.ts | 16 ++++ packages/test-utils/src/index.ts | 2 +- packages/test-utils/src/typeorm.ts | 13 ---- pnpm-lock.yaml | 10 ++- 21 files changed, 204 insertions(+), 190 deletions(-) create mode 100644 packages/test-utils/src/drizzle.ts delete mode 100644 packages/test-utils/src/typeorm.ts diff --git a/apps/auth-manager/src/asset/controllers/assetController.ts b/apps/auth-manager/src/asset/controllers/assetController.ts index aeb1792d..b4485c2c 100644 --- a/apps/auth-manager/src/asset/controllers/assetController.ts +++ b/apps/auth-manager/src/asset/controllers/assetController.ts @@ -3,8 +3,9 @@ import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import { type Logger } from '@map-colonies/js-logger'; import type { TypedRequestHandlers, components } from 'auth-openapi'; +import { Asset } from '@map-colonies/auth-core'; import { SERVICES } from '@common/constants'; -import { AssetManager, type ResponseAsset } from '../models/assetManager'; +import { AssetManager } from '../models/assetManager'; import { AssetNotFoundError, AssetVersionMismatchError } from '../models/errors'; /** @@ -12,9 +13,10 @@ import { AssetNotFoundError, AssetVersionMismatchError } from '../models/errors' * @param asset - The asset entity to convert * @returns The asset formatted according to OpenAPI schema */ -function responseAssetToOpenApi(asset: ResponseAsset): components['schemas']['asset'] { +function responseAssetToOpenApi(asset: Asset): components['schemas']['asset'] { return { ...asset, + value: asset.value.toString('base64'), createdAt: asset.createdAt.toISOString(), }; } diff --git a/apps/auth-manager/src/asset/models/assetManager.ts b/apps/auth-manager/src/asset/models/assetManager.ts index 6e7157df..bd0502ba 100644 --- a/apps/auth-manager/src/asset/models/assetManager.ts +++ b/apps/auth-manager/src/asset/models/assetManager.ts @@ -1,5 +1,5 @@ import { type Logger } from '@map-colonies/js-logger'; -import { IAsset } from '@map-colonies/auth-core'; +import { Asset, IAsset, NewAsset } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; import { ArrayContains } from 'typeorm'; import type { SetRequired } from 'type-fest'; @@ -8,9 +8,6 @@ import { SERVICES } from '@common/constants'; import { type AssetRepository } from '../DAL/assetRepository'; import { AssetVersionMismatchError, AssetNotFoundError } from './errors'; -export type ResponseAsset = SetRequired; -export type RequestAsset = Omit; - @injectable() export class AssetManager { public constructor( @@ -18,20 +15,20 @@ export class AssetManager { @inject(SERVICES.ASSET_REPOSITORY) private readonly assetRepository: AssetRepository ) {} - public async getAssets(searchParams: NonNullable): Promise { + public async getAssets(searchParams: NonNullable): Promise { this.logger.info({ msg: 'fetching assets', searchParams }); const { environment, isTemplate, type } = searchParams; return this.assetRepository.findBy({ environment: environment ? ArrayContains(environment) : undefined, isTemplate, type }); } - public async getNamedAssets(name: string): Promise { + public async getNamedAssets(name: string): Promise { this.logger.info({ msg: 'fetching all specific environment assets', asset: { name } }); return this.assetRepository.findBy({ name }); } - public async getAsset(name: string, version: number): Promise { + public async getAsset(name: string, version: number): Promise { this.logger.info({ msg: 'fetching asset', asset: { name, version } }); const asset = await this.assetRepository.findOne({ where: { name, version } }); @@ -43,7 +40,7 @@ export class AssetManager { return asset; } - public async getLatestAsset(name: string): Promise { + public async getLatestAsset(name: string): Promise { this.logger.info({ msg: 'fetching latest asset', asset: { name } }); const version = await this.assetRepository.getMaxVersion(name); if (version === null) { @@ -53,7 +50,7 @@ export class AssetManager { return this.getAsset(name, version); } - public async upsertAsset(asset: RequestAsset): Promise { + public async upsertAsset(asset: NewAsset): Promise { this.logger.info({ msg: 'upserting asset', asset: { environment: asset.environment, version: asset.version } }); return this.assetRepository.manager.transaction(async (transactionManager) => { const transactionRepo = transactionManager.withRepository(this.assetRepository); diff --git a/apps/auth-manager/src/common/db/pagination.ts b/apps/auth-manager/src/common/db/pagination.ts index 63d01036..4b5e4593 100644 --- a/apps/auth-manager/src/common/db/pagination.ts +++ b/apps/auth-manager/src/common/db/pagination.ts @@ -1,5 +1,5 @@ -import { SQL } from 'drizzle-orm'; -import { PgColumn, PgSelect } from 'drizzle-orm/pg-core'; +import type { SQL } from 'drizzle-orm'; +import type { PgColumn, PgSelect } from 'drizzle-orm/pg-core'; export const DEFAULT_PAGE_SIZE = 10; export interface PaginationParams { @@ -7,7 +7,7 @@ export interface PaginationParams { pageSize: number; } -export function paginationParamsToFindOptions(paginationParams?: PaginationParams): { take?: number; skip?: number } { +export function paginationParamsToOffsetAndLimit(paginationParams?: PaginationParams): { limit?: number; offset?: number } { if (paginationParams === undefined) { return {}; } @@ -15,8 +15,8 @@ export function paginationParamsToFindOptions(paginationParams?: PaginationParam const { page, pageSize } = paginationParams; return { - take: pageSize, - skip: (page - 1) * pageSize, + limit: pageSize, + offset: (page - 1) * pageSize, }; } diff --git a/apps/auth-manager/src/domain/models/domainManager.ts b/apps/auth-manager/src/domain/models/domainManager.ts index fe6856b8..eb593149 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -1,13 +1,10 @@ -import { type Logger } from '@map-colonies/js-logger'; -import { Domain, domainTable, Drizzle, IDomain } from '@map-colonies/auth-core'; +import type { Logger } from '@map-colonies/js-logger'; +import { Domain, domainTable, type NewDomain, type Drizzle } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -// import { FindManyOptions } from 'typeorm'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { where } from 'drizzle-orm'; -import { PaginationParams, paginationParamsToFindOptions } from '@src/common/db/pagination'; -import { SortOptions } from '@src/common/db/sort'; +import { count } from 'drizzle-orm'; +import { type PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; +import type { SortOptions } from '@src/common/db/sort'; import { SERVICES } from '@common/constants'; -// import { type DomainRepository } from '../DAL/domainRepository'; import { DomainAlreadyExistsError } from './errors'; @injectable() @@ -17,24 +14,28 @@ export class DomainManager { @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} - public async getDomains(paginationParams?: PaginationParams, sortParams?: SortOptions): Promise<[IDomain[], number]> { + public async getDomains(paginationParams?: PaginationParams, sortParams?: SortOptions): Promise<[Domain[], number]> { this.logger.info({ msg: 'fetching domains' }); - let findOptions: FindManyOptions = { - where: {}, - }; + let findOptions: Parameters[0] = {}; if (paginationParams !== undefined) { - findOptions = paginationParamsToFindOptions(paginationParams); + findOptions = { ...paginationParamsToOffsetAndLimit(paginationParams) }; } if (sortParams !== undefined) { - findOptions.order = sortParams; + findOptions.orderBy = sortParams; } - return this.drizzle.query.domain.findMany({ extras: {} }); + + const domainsQuery = this.drizzle.query.domain.findMany(findOptions); + const countQuery = this.drizzle.select({ count: count() }).from(domainTable); + + const result = await Promise.all([domainsQuery, countQuery]); + + return [result[0], result[1][0]?.count ?? 0]; // return this.domainRepository.findAndCount(findOptions); } - public async createDomain(domain: IDomain): Promise { + public async createDomain(domain: NewDomain): Promise { this.logger.info({ msg: 'creating domain', name: domain.name }); try { await this.domainRepository.insert(domain); diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index 8ca8f2fe..29945626 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -25,7 +25,7 @@ "lint:fix": "eslint --fix .", "prebuild": "pnpm run clean", "build": "tsc --project tsconfig.json && pnpm run assets:copy", - "assets:copy": "copyfiles ./package.json dist", + "assets:copy": "copyfiles -u 3 src/db/migrations/**/* dist/db/migrations", "clean": "rimraf dist", "prepack": "turbo run build", "check-dist": "publint && attw --pack .", diff --git a/packages/auth-core/src/db/entities/bundle.ts b/packages/auth-core/src/db/entities/bundle.ts index e175919f..aeb56186 100644 --- a/packages/auth-core/src/db/entities/bundle.ts +++ b/packages/auth-core/src/db/entities/bundle.ts @@ -41,9 +41,9 @@ export const bundleTable = authManagerSchema.table('bundle', { id: integer().primaryKey().generatedAlwaysAsIdentity(), hash: text(), environment: environmentEnum().notNull(), - metadata: jsonb(), - assets: jsonb(), - connections: jsonb(), + metadata: jsonb().$type>(), + assets: jsonb().$type<{ name: string; version: number }[]>(), + connections: jsonb().$type<{ name: string; version: number }[]>(), createdAt: createdAtColumn, keyVersion: integer(), opaVersion: text().notNull(), diff --git a/packages/auth-core/src/db/entities/client.ts b/packages/auth-core/src/db/entities/client.ts index e49067fc..76c8d016 100644 --- a/packages/auth-core/src/db/entities/client.ts +++ b/packages/auth-core/src/db/entities/client.ts @@ -3,6 +3,7 @@ import { json, text, timestamp } from 'drizzle-orm/pg-core'; import { authManagerSchema, createdAtColumn } from './common'; +import { PointOfContact } from '../../model'; // /** // * The typeorm implementation of the IClient interface. @@ -44,8 +45,8 @@ export const clientTable = authManagerSchema.table('client', { branch: text(), createdAt: createdAtColumn, updateAt: timestamp({ withTimezone: true }).defaultNow().notNull(), - techPointOfContact: json('tech_point_of_contact'), - productPointOfContact: json('product_point_of_contact'), + techPointOfContact: json('tech_point_of_contact').$type(), + productPointOfContact: json('product_point_of_contact').$type(), tags: text().array(), }); diff --git a/packages/auth-core/src/db/entities/key.ts b/packages/auth-core/src/db/entities/key.ts index 89e01883..3a194e8d 100644 --- a/packages/auth-core/src/db/entities/key.ts +++ b/packages/auth-core/src/db/entities/key.ts @@ -2,6 +2,7 @@ // import { Environment, type Environments, type IKey, type JWKPrivateKey, type JWKPublicKey } from '../../model'; import { integer, jsonb, primaryKey } from 'drizzle-orm/pg-core'; +import type { JWKPrivateKey, JWKPublicKey } from '../../model'; import { authManagerSchema, environmentEnum } from './common'; // /** @@ -27,8 +28,8 @@ export const keyTable = authManagerSchema.table( { environment: environmentEnum().notNull(), version: integer().notNull(), - privateKey: jsonb().notNull(), - publicKey: jsonb().notNull(), + privateKey: jsonb().notNull().$type(), + publicKey: jsonb().notNull().$type(), }, (table) => [primaryKey({ columns: [table.environment, table.version], name: 'PK_ddf3d991c46b66651794ee56d58' })] ); diff --git a/packages/auth-core/src/db/utils/createConnection.ts b/packages/auth-core/src/db/utils/createConnection.ts index bb7d210a..f38ac6d2 100644 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ b/packages/auth-core/src/db/utils/createConnection.ts @@ -33,6 +33,7 @@ export function createDrizzle(pool: Pool): Drizzle { export async function runMigrations(drizzle: NodePgDatabase): Promise { const optionalFolders = ['./db/migrations', './src/db/migrations', './migrations']; + console.log(__dirname); let migrationsFolder = null; for (const folder of optionalFolders) { diff --git a/packages/auth-core/src/model/asset.ts b/packages/auth-core/src/model/asset.ts index 43f4c296..fd3e5205 100644 --- a/packages/auth-core/src/model/asset.ts +++ b/packages/auth-core/src/model/asset.ts @@ -1,40 +1,40 @@ -import type { Environments } from './common'; +// import type { Environments } from './common'; -/* eslint-disable @typescript-eslint/naming-convention */ -export const AssetType = { - /** OPA test files. */ - TEST: 'TEST', - TEST_DATA: 'TEST_DATA', - /** OPA policy files. */ - POLICY: 'POLICY', - /** OPA data files, name should end with .json or .yaml. */ - DATA: 'DATA', -} as const; -/* eslint-enable @typescript-eslint/naming-convention */ +// /* eslint-disable @typescript-eslint/naming-convention */ +// export const AssetType = { +// /** OPA test files. */ +// TEST: 'TEST', +// TEST_DATA: 'TEST_DATA', +// /** OPA policy files. */ +// POLICY: 'POLICY', +// /** OPA data files, name should end with .json or .yaml. */ +// DATA: 'DATA', +// } as const; +// /* eslint-enable @typescript-eslint/naming-convention */ -export type AssetTypes = (typeof AssetType)[keyof typeof AssetType]; +// export type AssetTypes = (typeof AssetType)[keyof typeof AssetType]; -/** - * Describes the metadata and content of assets - files that will be part of the bundle. - */ -export interface IAsset { - /** The unique name of the asset. */ - name: string; - /** - * The version of Asset with the given name. Starts at 1 and automatically increments. - * When updated, the POST body should contain the latest version. - */ - version: number; - /** Automatically generated date when the given asset version was created. */ - createdAt?: Date; - /** Base64 encoded value of the asset file. */ - value: string; - /** The path inside the bundle the asset will be in. use / for the root of the bundle. */ - uri: string; - /** The asset type. */ - type: AssetTypes; - /** The environments the asset belongs do. It will be deployed only to the specified environments. */ - environment: Environments[]; - /** Whether the file contains a template that should be rendered before inserting to the bundle. */ - isTemplate: boolean; -} +// /** +// * Describes the metadata and content of assets - files that will be part of the bundle. +// */ +// export interface IAsset { +// /** The unique name of the asset. */ +// name: string; +// /** +// * The version of Asset with the given name. Starts at 1 and automatically increments. +// * When updated, the POST body should contain the latest version. +// */ +// version: number; +// /** Automatically generated date when the given asset version was created. */ +// createdAt?: Date; +// /** Base64 encoded value of the asset file. */ +// value: string; +// /** The path inside the bundle the asset will be in. use / for the root of the bundle. */ +// uri: string; +// /** The asset type. */ +// type: AssetTypes; +// /** The environments the asset belongs do. It will be deployed only to the specified environments. */ +// environment: Environments[]; +// /** Whether the file contains a template that should be rendered before inserting to the bundle. */ +// isTemplate: boolean; +// } diff --git a/packages/auth-core/src/model/bundle.ts b/packages/auth-core/src/model/bundle.ts index d6a1d38e..891e89bb 100644 --- a/packages/auth-core/src/model/bundle.ts +++ b/packages/auth-core/src/model/bundle.ts @@ -1,25 +1,25 @@ -import type { Environments } from './common'; +// import type { Environments } from './common'; /** * Describes the metadata of contents of bundles that were created. */ -export interface IBundle { - /** The auto-generated ID of the bundle. */ - id?: number; - /** The environment the bundle was created for. */ - environment: Environments; - /** The md5 based hash of the bundle tarball. */ - hash?: string; - /** Free form object to describe the bundle. */ - metadata?: Record; - /** A list of all the assets that are part of the bundle. */ - assets?: { name: string; version: number }[]; - /** A list of all the connections that are part of the bundle. */ - connections?: { name: string; version: number }[]; - /** Automatically generated date when the given bundle was created. */ - createdAt?: Date; - /** The version of the key that is part of the bundle. */ - keyVersion?: number; - /** The version of the OPA cli that was used to create the bundle. */ - opaVersion: string; -} +// export interface IBundle { +// /** The auto-generated ID of the bundle. */ +// id?: number; +// /** The environment the bundle was created for. */ +// environment: Environments; +// /** The md5 based hash of the bundle tarball. */ +// hash?: string; +// /** Free form object to describe the bundle. */ +// metadata?: Record; +// /** A list of all the assets that are part of the bundle. */ +// assets?: { name: string; version: number }[]; +// /** A list of all the connections that are part of the bundle. */ +// connections?: { name: string; version: number }[]; +// /** Automatically generated date when the given bundle was created. */ +// createdAt?: Date; +// /** The version of the key that is part of the bundle. */ +// keyVersion?: number; +// /** The version of the OPA cli that was used to create the bundle. */ +// opaVersion: string; +// } diff --git a/packages/auth-core/src/model/client.ts b/packages/auth-core/src/model/client.ts index 0d2f3fe0..dfc3c4ea 100644 --- a/packages/auth-core/src/model/client.ts +++ b/packages/auth-core/src/model/client.ts @@ -8,26 +8,26 @@ export interface PointOfContact { email: string; } -/** - * Describes a specific authentication client. e.g. a system not a person. - */ -export interface IClient { - /** The name of the client. */ - name: string; - /** The name of the client in hebrew. */ - hebName: string; - /** A short description about the client. */ - description?: string; - /** The branch the client belongs to. */ - branch?: string; - /** Automatically generated date of when the given client was created at. */ - createdAt?: Date; - /** Automatically generated date of when the given client was updated at. */ - updatedAt?: Date; - /** The contact details of the person in charge of tech at the client. */ - techPointOfContact?: PointOfContact; - /** The contact details of the person in charge of product at the client. */ - productPointOfContact?: PointOfContact; - /** The tags describing the client. */ - tags?: string[]; -} +// /** +// * Describes a specific authentication client. e.g. a system not a person. +// */ +// export interface IClient { +// /** The name of the client. */ +// name: string; +// /** The name of the client in hebrew. */ +// hebName: string; +// /** A short description about the client. */ +// description?: string; +// /** The branch the client belongs to. */ +// branch?: string; +// /** Automatically generated date of when the given client was created at. */ +// createdAt?: Date; +// /** Automatically generated date of when the given client was updated at. */ +// updatedAt?: Date; +// /** The contact details of the person in charge of tech at the client. */ +// techPointOfContact?: PointOfContact; +// /** The contact details of the person in charge of product at the client. */ +// productPointOfContact?: PointOfContact; +// /** The tags describing the client. */ +// tags?: string[]; +// } diff --git a/packages/auth-core/src/model/common.ts b/packages/auth-core/src/model/common.ts index daec024d..73d6099c 100644 --- a/packages/auth-core/src/model/common.ts +++ b/packages/auth-core/src/model/common.ts @@ -1,13 +1,13 @@ /** The possible authentication deployment environments. */ /* eslint-disable @typescript-eslint/naming-convention */ -export const Environment = { - /** Non production, may also be called dev. */ - NP: 'np', - /** The staging environment, may also be called integration. */ - STAGE: 'stage', - /** The production environment. */ - PRODUCTION: 'prod', -} as const; -/* eslint-enable @typescript-eslint/naming-convention */ +// export const Environment = { +// /** Non production, may also be called dev. */ +// NP: 'np', +// /** The staging environment, may also be called integration. */ +// STAGE: 'stage', +// /** The production environment. */ +// PRODUCTION: 'prod', +// } as const; +// /* eslint-enable @typescript-eslint/naming-convention */ -export type Environments = (typeof Environment)[keyof typeof Environment]; +// export type Environments = (typeof Environment)[keyof typeof Environment]; diff --git a/packages/auth-core/src/model/connection.ts b/packages/auth-core/src/model/connection.ts index 5bc0ddab..d36f9be0 100644 --- a/packages/auth-core/src/model/connection.ts +++ b/packages/auth-core/src/model/connection.ts @@ -1,31 +1,31 @@ -import type { Environments } from './common'; +// import type { Environments } from './common'; /** * A connection is the object describing the details of * how a specific clients authenticates to a specific {@link Environment}. */ -export interface IConnection { - /** The name of the clients this connection relates to. */ - name: string; - /** - * The version of the connection with the given {@link name} and {@link environment}. Starts at 1 and automatically increments. - * When updated, the POST body should contain the latest version. - */ - version: number; - /** The environment this connection relates to. */ - environment: Environments; - /** Automatically generated date of when the given connection version was created at. */ - createdAt?: Date; - /** Is the connection enabled. If it is not, it wwill be ignored when creating a new bundle. */ - enabled: boolean; - /** The client's token for the specific environment. The KID parameter in the token should equal the client's name. */ - token: string; - /** Decides if requests that are not originated from a browser are allowed. */ - allowNoBrowserConnection: boolean; - /** Decides if requests that are missing the Origin header are allowed. */ - allowNoOriginConnection: boolean; - /** A list of domains that the client is allowed to send request to. */ - domains: string[]; - /** A list of origins the client is allowed to send requests from. */ - origins: string[]; -} +// export interface IConnection { +// /** The name of the clients this connection relates to. */ +// name: string; +// /** +// * The version of the connection with the given {@link name} and {@link environment}. Starts at 1 and automatically increments. +// * When updated, the POST body should contain the latest version. +// */ +// version: number; +// /** The environment this connection relates to. */ +// environment: Environments; +// /** Automatically generated date of when the given connection version was created at. */ +// createdAt?: Date; +// /** Is the connection enabled. If it is not, it wwill be ignored when creating a new bundle. */ +// enabled: boolean; +// /** The client's token for the specific environment. The KID parameter in the token should equal the client's name. */ +// token: string; +// /** Decides if requests that are not originated from a browser are allowed. */ +// allowNoBrowserConnection: boolean; +// /** Decides if requests that are missing the Origin header are allowed. */ +// allowNoOriginConnection: boolean; +// /** A list of domains that the client is allowed to send request to. */ +// domains: string[]; +// /** A list of origins the client is allowed to send requests from. */ +// origins: string[]; +// } diff --git a/packages/auth-core/src/model/domain.ts b/packages/auth-core/src/model/domain.ts index 808cb42e..bc112164 100644 --- a/packages/auth-core/src/model/domain.ts +++ b/packages/auth-core/src/model/domain.ts @@ -1,6 +1,6 @@ /** * A domain describes a part of the MapColonies system. */ -export interface IDomain { - name: string; -} +// export interface IDomain { +// name: string; +// } diff --git a/packages/auth-core/src/model/key.ts b/packages/auth-core/src/model/key.ts index 61b77d27..d8e324cc 100644 --- a/packages/auth-core/src/model/key.ts +++ b/packages/auth-core/src/model/key.ts @@ -1,4 +1,4 @@ -import type { Environments } from './common'; +// import type { Environments } from './common'; /** * JSON representation of a public key @@ -26,14 +26,14 @@ export interface JWKPrivateKey extends JWKPublicKey { /** * A representation of a authentication key for a specific environment. */ -export interface IKey { - /** - * The version of the key with the given {@link environment}. Starts at 1 and automatically increments. - * When updated, the POST body should contain the latest version. - */ - environment: Environments; - /** The environment this key relates to. */ - version: number; - privateKey: JWKPrivateKey; - publicKey: JWKPublicKey; -} +// export interface IKey { +// /** +// * The version of the key with the given {@link environment}. Starts at 1 and automatically increments. +// * When updated, the POST body should contain the latest version. +// */ +// environment: Environments; +// /** The environment this key relates to. */ +// version: number; +// privateKey: JWKPrivateKey; +// publicKey: JWKPublicKey; +// } diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index c3bca6c8..5bbdd9be 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -37,8 +37,9 @@ "@map-colonies/schemas": "catalog:", "@testcontainers/minio": "~11.14.0", "@testcontainers/postgresql": "^11.14.0", - "typeorm": "catalog:", - "lodash": "^4.18.1" + "lodash": "^4.18.1", + "drizzle-orm": "catalog:", + "pg": "catalog:" }, "peerDependencies": { "vitest": "catalog:" @@ -47,6 +48,7 @@ "@map-colonies/eslint-config": "catalog:", "@map-colonies/tsconfig": "catalog:", "@types/node": "catalog:", + "@types/pg": "catalog:", "eslint": "catalog:", "typescript": "catalog:", "@types/lodash": "^4.17.24" diff --git a/packages/test-utils/src/drizzle.ts b/packages/test-utils/src/drizzle.ts new file mode 100644 index 00000000..0d5bd001 --- /dev/null +++ b/packages/test-utils/src/drizzle.ts @@ -0,0 +1,16 @@ +import { runMigrations, authManagerSchema, createDrizzle } from '@map-colonies/auth-core'; +import type { Pool } from 'pg'; +import type { commonDbFullV1Type } from '@map-colonies/schemas'; + +/** + * Drops and recreates the schema, then runs all pending migrations on the given connection. + */ +export async function resetAndMigrate(connection: Pool): Promise { + await connection.query(`DROP SCHEMA IF EXISTS "${authManagerSchema.schemaName}" CASCADE`); + await connection.query(`CREATE SCHEMA "${authManagerSchema.schemaName}"`); + + const db = createDrizzle(connection); + await runMigrations(db); +} + +export type { commonDbFullV1Type as TypeormConnectionOptions }; diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 4bba3cc3..fc08e053 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -2,5 +2,5 @@ export * from './containers.js'; export * from './fakers.js'; export * from './fs.js'; export * from './s3.js'; -export * from './typeorm.js'; +export * from './drizzle.js'; export * from './config.js'; diff --git a/packages/test-utils/src/typeorm.ts b/packages/test-utils/src/typeorm.ts deleted file mode 100644 index 66751290..00000000 --- a/packages/test-utils/src/typeorm.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type DataSource } from 'typeorm'; -import type { commonDbFullV1Type } from '@map-colonies/schemas'; - -/** - * Drops and recreates the schema, then runs all pending migrations on the given connection. - */ -export async function resetAndMigrate(connection: DataSource, schema: string): Promise { - await connection.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`); - await connection.query(`CREATE SCHEMA "${schema}"`); - await connection.runMigrations(); -} - -export type { commonDbFullV1Type as TypeormConnectionOptions }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b4bd778..a8a210b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1050,12 +1050,15 @@ importers: '@testcontainers/postgresql': specifier: ^11.14.0 version: 11.14.0 + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) lodash: specifier: ^4.18.1 version: 4.18.1 - typeorm: + pg: specifier: 'catalog:' - version: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)) + version: 8.20.0 vitest: specifier: 'catalog:' version: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -1072,6 +1075,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.12.0 + '@types/pg': + specifier: 'catalog:' + version: 8.20.0 eslint: specifier: 'catalog:' version: 9.39.4(jiti@2.6.1) From a0f662ae9eb331ab9c0a04ed3ed51a4224fddadb Mon Sep 17 00:00:00 2001 From: CptSchnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Sun, 17 May 2026 15:10:39 +0300 Subject: [PATCH 05/14] chore(global): more progress --- .../src/asset/DAL/assetRepository.ts | 84 +++++------ .../src/asset/controllers/assetController.ts | 4 +- .../src/asset/models/assetManager.ts | 43 ++++-- .../bundle/controllers/bundleController.ts | 9 +- apps/auth-manager/src/bundle/models/bundle.ts | 8 +- .../src/bundle/models/bundleManager.ts | 25 ++-- .../src/client/DAL/clientRepository.ts | 28 ---- .../client/controllers/clientController.ts | 16 +- .../src/client/models/clientManager.ts | 107 +++++++------ apps/auth-manager/src/common/db/constants.ts | 7 +- apps/auth-manager/src/common/db/pagination.ts | 2 +- apps/auth-manager/src/common/db/utils.ts | 11 +- apps/auth-manager/src/containerConfig.ts | 21 ++- .../src/domain/models/domainManager.ts | 2 +- apps/auth-manager/src/utils/mapper.ts | 22 +++ .../tests/integration/domain/domain.spec.mts | 20 +-- .../unit/asset/models/assetManager.spec.mts | 80 +--------- .../unit/bundle/models/bundleModel.spec.mts | 68 --------- .../unit/client/models/clientManager.spec.mts | 98 ------------ .../models/connectionManager.spec.mts | 140 ------------------ .../unit/domain/models/domainModel.spec.mts | 67 --------- packages/auth-core/src/db/entities/client.ts | 4 +- .../migration.sql | 2 +- .../src/db/utils/createConnection.ts | 8 +- packages/auth-core/src/model/asset.ts | 25 ++-- packages/auth-core/src/model/common.ts | 21 +-- packages/test-utils/src/drizzle.ts | 2 +- packages/test-utils/src/fakers.ts | 43 ++++-- 28 files changed, 284 insertions(+), 683 deletions(-) delete mode 100644 apps/auth-manager/src/client/DAL/clientRepository.ts create mode 100644 apps/auth-manager/src/utils/mapper.ts delete mode 100644 apps/auth-manager/tests/unit/bundle/models/bundleModel.spec.mts delete mode 100644 apps/auth-manager/tests/unit/domain/models/domainModel.spec.mts diff --git a/apps/auth-manager/src/asset/DAL/assetRepository.ts b/apps/auth-manager/src/asset/DAL/assetRepository.ts index dec943e8..b941ea94 100644 --- a/apps/auth-manager/src/asset/DAL/assetRepository.ts +++ b/apps/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,46 +1,38 @@ -import { assetTable } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; - -export type AssetRepository = Repository & { - getMaxVersionWithLock: (name: string) => Promise; - getMaxVersion: (name: string) => Promise; -}; - -export const assetRepositoryFactory: FactoryFunction = (container) => { - const dataSource = container.resolve(DataSource); - - return dataSource.getRepository(assetTable).extend({ - async getMaxVersionWithLock(name: string): Promise { - const result = await this.createQueryBuilder() - .select('version') - .where('name = :name') - .andWhere((qb) => { - const subQuery = qb.subQuery().select('MAX(version)').from(assetTable, 'asset').where('name = :name').getQuery(); - - return 'Asset.version = ' + subQuery; - }) - .setLock('pessimistic_write') - .setParameter('name', name) - .getRawOne<{ version: number }>(); - - if (result === undefined) { - return null; - } - return result.version; - }, - async getMaxVersion(name: string): Promise { - const result = await this.createQueryBuilder() - .select('MAX(version)', 'version') - .where('name = :name') - .setParameter('name', name) - .getRawOne<{ version: number }>(); - - if (result === undefined) { - return null; - } - return result.version; - }, - }); -}; +import { and, eq, max } from 'drizzle-orm'; +import { assetTable, type Drizzle } from '@map-colonies/auth-core'; +import { inject, injectable } from 'tsyringe'; +import { SERVICES } from '@common/constants'; + +type DrizzleTx = Parameters[0]>[0]; + +@injectable() +export class AssetRepository { + public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} + + public async getMaxVersionWithLock(name: string, tx: DrizzleTx): Promise { + const subQuery = tx + .select({ maxVersion: max(assetTable.version) }) + .from(assetTable) + .where(eq(assetTable.name, name)); + + const result = await tx + .select({ version: assetTable.version }) + .from(assetTable) + .where(and(eq(assetTable.name, name), eq(assetTable.version, subQuery))) + .for('update') + .limit(1); + + return result[0]?.version ?? null; + } + + public async getMaxVersion(name: string, tx?: DrizzleTx): Promise { + const db = tx ?? this.db; + + const result = await db + .select({ version: max(assetTable.version) }) + .from(assetTable) + .where(eq(assetTable.name, name)); + + return result[0]?.version ?? null; + } +} diff --git a/apps/auth-manager/src/asset/controllers/assetController.ts b/apps/auth-manager/src/asset/controllers/assetController.ts index b4485c2c..ae123da5 100644 --- a/apps/auth-manager/src/asset/controllers/assetController.ts +++ b/apps/auth-manager/src/asset/controllers/assetController.ts @@ -96,8 +96,10 @@ export class AssetController { public upsertAsset: TypedRequestHandlers['upsertAsset'] = async (req, res, next) => { this.logger.debug('executing #upsertAsset handler'); + const { createdAt, value, ...rest } = req.body; + try { - const createdAsset = await this.manager.upsertAsset(req.body); + const createdAsset = await this.manager.upsertAsset({ ...rest, value: Buffer.from(value, 'base64') }); const returnStatus = createdAsset.version === 1 ? httpStatus.CREATED : httpStatus.OK; return res.status(returnStatus).json(responseAssetToOpenApi(createdAsset)); diff --git a/apps/auth-manager/src/asset/models/assetManager.ts b/apps/auth-manager/src/asset/models/assetManager.ts index bd0502ba..8f806ae9 100644 --- a/apps/auth-manager/src/asset/models/assetManager.ts +++ b/apps/auth-manager/src/asset/models/assetManager.ts @@ -1,39 +1,48 @@ import { type Logger } from '@map-colonies/js-logger'; -import { Asset, IAsset, NewAsset } from '@map-colonies/auth-core'; +import { Asset, assetTable, type Drizzle, NewAsset } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -import { ArrayContains } from 'typeorm'; -import type { SetRequired } from 'type-fest'; +import { and, eq } from 'drizzle-orm'; import { operations } from 'auth-openapi'; import { SERVICES } from '@common/constants'; -import { type AssetRepository } from '../DAL/assetRepository'; +import { AssetRepository } from '../DAL/assetRepository'; import { AssetVersionMismatchError, AssetNotFoundError } from './errors'; @injectable() export class AssetManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.ASSET_REPOSITORY) private readonly assetRepository: AssetRepository + @inject(AssetRepository) private readonly assetRepository: AssetRepository, + @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} public async getAssets(searchParams: NonNullable): Promise { this.logger.info({ msg: 'fetching assets', searchParams }); const { environment, isTemplate, type } = searchParams; - return this.assetRepository.findBy({ environment: environment ? ArrayContains(environment) : undefined, isTemplate, type }); + // return this.assetRepository.findBy({ environment: environment ? ArrayContains(environment) : undefined, isTemplate, type }); + return this.drizzle.query.asset.findMany({ + where: { + isTemplate, + type, + environment: environment ? { arrayContains: environment } : undefined, + }, + }); } public async getNamedAssets(name: string): Promise { this.logger.info({ msg: 'fetching all specific environment assets', asset: { name } }); - return this.assetRepository.findBy({ name }); + // return this.assetRepository.findBy({ name }); + return this.drizzle.query.asset.findMany({ where: { name } }); } public async getAsset(name: string, version: number): Promise { this.logger.info({ msg: 'fetching asset', asset: { name, version } }); - const asset = await this.assetRepository.findOne({ where: { name, version } }); + // const asset = await this.assetRepository.findOne({ where: { name, version } }); + const asset = await this.drizzle.query.asset.findFirst({ where: { name, version } }); - if (asset === null) { + if (asset === undefined) { this.logger.debug('asset was not found in the database'); throw new AssetNotFoundError('asset was not found in the database'); } @@ -52,10 +61,9 @@ export class AssetManager { public async upsertAsset(asset: NewAsset): Promise { this.logger.info({ msg: 'upserting asset', asset: { environment: asset.environment, version: asset.version } }); - return this.assetRepository.manager.transaction(async (transactionManager) => { - const transactionRepo = transactionManager.withRepository(this.assetRepository); - const maxVersion = await transactionRepo.getMaxVersionWithLock(asset.name); + return this.drizzle.transaction(async (tx) => { + const maxVersion = await this.assetRepository.getMaxVersionWithLock(asset.name, tx); if (maxVersion === null) { if (asset.version !== 1) { @@ -65,18 +73,23 @@ export class AssetManager { } // insert - return transactionRepo.save(asset); + return (await tx.insert(assetTable).values(asset).returning())[0] as Asset; } if (maxVersion !== asset.version) { const msg = 'version mismatch between database asset and given asset'; this.logger.debug({ msg, clientAssetVersion: asset.version, dbAssetVersion: maxVersion }); - throw new AssetVersionMismatchError(msg); } // update - return transactionRepo.save({ ...asset, version: maxVersion + 1 }); + return ( + await tx + .update(assetTable) + .set({ ...asset, version: maxVersion + 1 }) + .where(and(eq(assetTable.name, asset.name), eq(assetTable.version, maxVersion))) + .returning() + )[0] as Asset; }); } } diff --git a/apps/auth-manager/src/bundle/controllers/bundleController.ts b/apps/auth-manager/src/bundle/controllers/bundleController.ts index 3be21d0c..ca125da3 100644 --- a/apps/auth-manager/src/bundle/controllers/bundleController.ts +++ b/apps/auth-manager/src/bundle/controllers/bundleController.ts @@ -1,15 +1,16 @@ import { HttpError } from '@map-colonies/error-express-handler'; -import { IBundle } from '@map-colonies/auth-core'; +import { Bundle } from '@map-colonies/auth-core'; import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import type { TypedRequestHandlers, components, operations } from 'auth-openapi'; +import { removeNulls } from '@src/utils/mapper'; import { BundleManager } from '../models/bundleManager'; import { BundleNotFoundError } from '../models/errors'; -function responseBundleToOpenApi(bundle: IBundle): components['schemas']['bundle'] { +function responseBundleToOpenApi(bundle: Bundle): components['schemas']['bundle'] { return { - ...bundle, - createdAt: bundle.createdAt?.toISOString(), + ...removeNulls(bundle), + createdAt: bundle.createdAt.toISOString(), }; } diff --git a/apps/auth-manager/src/bundle/models/bundle.ts b/apps/auth-manager/src/bundle/models/bundle.ts index 55ef87c7..03e26226 100644 --- a/apps/auth-manager/src/bundle/models/bundle.ts +++ b/apps/auth-manager/src/bundle/models/bundle.ts @@ -1,7 +1,7 @@ -import type { IBundle } from '@map-colonies/auth-core'; +import type { Bundle } from '@map-colonies/auth-core'; export interface BundleSearchParams { - environment?: IBundle['environment'][]; - createdBefore?: IBundle['createdAt']; - createdAfter?: IBundle['createdAt']; + environment?: Bundle['environment'][]; + createdBefore?: Bundle['createdAt']; + createdAfter?: Bundle['createdAt']; } diff --git a/apps/auth-manager/src/bundle/models/bundleManager.ts b/apps/auth-manager/src/bundle/models/bundleManager.ts index 3d36a995..8c9edca9 100644 --- a/apps/auth-manager/src/bundle/models/bundleManager.ts +++ b/apps/auth-manager/src/bundle/models/bundleManager.ts @@ -1,9 +1,8 @@ import { type Logger } from '@map-colonies/js-logger'; -import { Bundle, IBundle } from '@map-colonies/auth-core'; +import type { Bundle, Drizzle } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -import { In, Repository } from 'typeorm'; +// import { In, Repository } from 'typeorm'; import { SERVICES } from '@common/constants'; -import { createDatesComparison } from '@common/db/utils'; import { BundleSearchParams } from './bundle'; import { BundleNotFoundError } from './errors'; @@ -11,27 +10,31 @@ import { BundleNotFoundError } from './errors'; export class BundleManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.BUNDLE_REPOSITORY) private readonly bundleRepository: Repository + @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} - public async getBundles(searchParams: BundleSearchParams): Promise { + public async getBundles(searchParams: BundleSearchParams): Promise { this.logger.info({ msg: 'fetching bundles' }); this.logger.debug({ msg: 'search parameters', searchParams }); const { createdAfter, createdBefore, environment } = searchParams; - return this.bundleRepository.findBy({ - createdAt: createDatesComparison(createdAfter, createdBefore), - environment: environment ? In(environment) : undefined, + return this.drizzle.query.bundle.findMany({ + where: { + // createdAt: createDatesComparison(createdAfter, createdBefore), + environment: environment ? { in: environment } : undefined, + createdAt: { gte: createdAfter, lte: createdBefore }, + }, }); } - public async getBundle(id: number): Promise { + public async getBundle(id: number): Promise { this.logger.info({ msg: 'fetching bundle', id }); - const bundle = await this.bundleRepository.findOneBy({ id }); + // const bundle = await this.bundleRepository.findOneBy({ id }); + const bundle = await this.drizzle.query.bundle.findFirst({ where: { id } }); - if (bundle === null) { + if (bundle === undefined) { this.logger.debug('bundle was not found in the database'); throw new BundleNotFoundError('bundle was not found in the database'); } diff --git a/apps/auth-manager/src/client/DAL/clientRepository.ts b/apps/auth-manager/src/client/DAL/clientRepository.ts deleted file mode 100644 index 2d4301c8..00000000 --- a/apps/auth-manager/src/client/DAL/clientRepository.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Client } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; - -export type ClientRepository = Repository & { updateAndReturn: (client: Client) => Promise }; - -export const clientRepositoryFactory: FactoryFunction = (container) => { - const dataSource = container.resolve(DataSource); - - return dataSource.getRepository(Client).extend({ - async updateAndReturn(client: Client): Promise { - return this.manager.transaction(async (transactionManager) => { - const dbClient = await transactionManager - .createQueryBuilder(Client, 'client') - .where('name = :name', { name: client.name }) - .setLock('pessimistic_write') - .getOne(); - - if (dbClient === null) { - return null; - } - - return transactionManager.save(Client, { ...dbClient, ...client }); - }); - }, - }); -}; diff --git a/apps/auth-manager/src/client/controllers/clientController.ts b/apps/auth-manager/src/client/controllers/clientController.ts index 3e97c352..13a6db01 100644 --- a/apps/auth-manager/src/client/controllers/clientController.ts +++ b/apps/auth-manager/src/client/controllers/clientController.ts @@ -2,21 +2,22 @@ import { HttpError } from '@map-colonies/error-express-handler'; import { type Logger } from '@map-colonies/js-logger'; import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; -import { IClient } from '@map-colonies/auth-core'; +import { Client } from '@map-colonies/auth-core'; import { parseISO } from 'date-fns'; import type { TypedRequestHandlers, components, operations } from 'auth-openapi'; import { SERVICES } from '@common/constants'; import { DEFAULT_PAGE_SIZE } from '@src/common/db/pagination'; +import { removeNulls } from '@src/utils/mapper'; import { sortOptionParser } from '@src/common/db/sort'; import { ClientManager } from '../models/clientManager'; import { ClientAlreadyExistsError, ClientNotFoundError } from '../models/errors'; import { ClientSearchParams } from '../models/client'; -function responseClientToOpenApi(client: IClient): components['schemas']['client'] { +function responseClientToOpenApi(client: Client): components['schemas']['client'] { return { - ...client, - createdAt: (client.createdAt as Date).toISOString(), - updatedAt: (client.updatedAt as Date).toISOString(), + ...removeNulls(client), + createdAt: client.createdAt.toISOString(), + updatedAt: client.updatedAt.toISOString(), }; } @@ -31,7 +32,7 @@ function queryParamsToSearchParams(query: NonNullable( +const clientSortMap = new Map( Object.entries({ name: 'name', branch: 'branch', @@ -104,7 +105,8 @@ export class ClientController { public updateClient: TypedRequestHandlers['updateClient'] = async (req, res, next) => { try { this.logger.debug('executing #updateClient handler'); - const updatedClient = await this.manager.updateClient(req.params.clientName, req.body); + const { createdAt, updatedAt, ...client } = req.body; + const updatedClient = await this.manager.updateClient({ ...client, name: req.params.clientName }); return res.status(httpStatus.OK).json(responseClientToOpenApi(updatedClient)); } catch (error) { if (error instanceof ClientNotFoundError) { diff --git a/apps/auth-manager/src/client/models/clientManager.ts b/apps/auth-manager/src/client/models/clientManager.ts index bade8dbd..07d167e5 100644 --- a/apps/auth-manager/src/client/models/clientManager.ts +++ b/apps/auth-manager/src/client/models/clientManager.ts @@ -1,18 +1,18 @@ import { type Logger } from '@map-colonies/js-logger'; import { inject, injectable } from 'tsyringe'; -import { ArrayContains, ILike, QueryFailedError } from 'typeorm'; +// import { ArrayContains, ILike, QueryFailedError } from 'typeorm'; import { DatabaseError } from 'pg'; -import { Client, type IClient } from '@map-colonies/auth-core'; +import { count, DrizzleQueryError, eq, and, arrayContains, between } from 'drizzle-orm'; +import { clientTable, type Client, type Drizzle, type NewClient } from '@map-colonies/auth-core'; import { SERVICES } from '@common/constants'; -import { PgErrorCodes } from '@common/db/constants'; +import { pgErrorCodes } from '@common/db/constants'; import { createDatesComparison } from '@common/db/utils'; import { SortOptions } from '@src/common/db/sort'; -import { PaginationParams, paginationParamsToFindOptions } from '@src/common/db/pagination'; -import { type ClientRepository } from '../DAL/clientRepository'; +import { PaginationParams, paginationParamsToFindOptions, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import { ClientAlreadyExistsError, ClientNotFoundError } from './errors'; import { ClientSearchParams } from './client'; -function isQueryFailedError(err: unknown): err is QueryFailedError { +function isQueryFailedError(err: unknown): err is DrizzleQueryError { return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'QueryFailedError'; } @@ -20,52 +20,71 @@ function isQueryFailedError(err: unknown): err is QueryFailedError { export class ClientManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.CLIENT_REPOSITORY) private readonly clientRepository: ClientRepository + @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} public async getClients( searchParams: ClientSearchParams, paginationParams?: PaginationParams, sortParams?: SortOptions - ): Promise<[IClient[], number]> { + ): Promise<[Client[], number]> { this.logger.info({ msg: 'fetching clients' }); this.logger.debug({ msg: 'search parameters', searchParams }); // eslint doesn't recognize this as valid because its in the type definition - let findOptions: Parameters[0] = {}; + // let findOptions: Parameters[0] = {}; const { name, branch, tags, createdAfter, createdBefore, updatedAfter, updatedBefore } = searchParams; - findOptions = { - where: { - name: name !== undefined ? ILike(`%${name}%`) : undefined, - tags: tags ? ArrayContains(tags) : undefined, - branch, - createdAt: createDatesComparison(createdAfter, createdBefore), - updatedAt: createDatesComparison(updatedAfter, updatedBefore), - }, - }; - - if (paginationParams !== undefined) { - findOptions = { - ...findOptions, - ...paginationParamsToFindOptions(paginationParams), - }; - } - - if (sortParams !== undefined) { - findOptions.order = sortParams; - } - - return this.clientRepository.findAndCount({ ...findOptions }); + // findOptions = { + // where: { + // name: name !== undefined ? ILike(`%${name}%`) : undefined, + // tags: tags ? ArrayContains(tags) : undefined, + // branch, + // createdAt: createDatesComparison(createdAfter, createdBefore), + // updatedAt: createDatesComparison(updatedAfter, updatedBefore), + // }, + // }; + + // if (paginationParams !== undefined) { + // findOptions = { + // ...findOptions, + // ...paginationParamsToFindOptions(paginationParams), + // }; + // } + + // if (sortParams !== undefined) { + // findOptions.order = sortParams; + // } + + // return this.clientRepository.findAndCount({ ...findOptions }); + + // let findOptions: Parameters[0] = { + // where: { + // }, + // }; + + const whereClause = and( + name !== undefined ? eq(clientTable.name, name) : undefined, + branch !== undefined ? eq(clientTable.branch, branch) : undefined, + tags !== undefined ? arrayContains(clientTable.tags, tags) : undefined, + createDatesComparison(clientTable.createdAt, createdAfter, createdBefore), + createDatesComparison(clientTable.updatedAt, updatedAfter, updatedBefore) + ); + + const { limit, offset } = paginationParamsToOffsetAndLimit(paginationParams); + + const clients = await this.drizzle.select().from(clientTable).where(whereClause).limit(limit).offset(offset).orderBy(sortParams); + + const countResult = await this.drizzle.select({ count: count() }).from(clientTable).where(whereClause); } - public async getClient(name: string): Promise { + public async getClient(name: string): Promise { this.logger.info({ msg: 'fetching client', name }); - const client = await this.clientRepository.findOne({ where: { name } }); + const client = await this.drizzle.query.client.findFirst({ where: { name } }); this.logger.debug('client result returned from db'); - if (client === null) { + if (client === undefined) { this.logger.debug('client result was null'); throw new ClientNotFoundError("A client with the given name doesn't exists in the database"); } @@ -73,16 +92,16 @@ export class ClientManager { return client; } - public async createClient(client: IClient): Promise { + public async createClient(client: NewClient): Promise { this.logger.info({ msg: 'creating domain', name: client.name }); try { - await this.clientRepository.insert(client); + const res = await this.drizzle.insert(clientTable).values(client).returning(); this.logger.debug('client result returned from db'); - return client; + return res[0] as Client; } catch (error) { - if (isQueryFailedError(error) && error.driverError instanceof DatabaseError && error.driverError.code === PgErrorCodes.UNIQUE_VIOLATION) { + if (isQueryFailedError(error) && error.cause instanceof DatabaseError && error.cause.code === pgErrorCodes.UNIQUE_VIOLATION) { throw new ClientAlreadyExistsError('client already exists'); } this.logger.debug('create client throw an unrecognized error'); @@ -90,19 +109,19 @@ export class ClientManager { } } - public async updateClient(name: string, client: Omit): Promise { - this.logger.info({ msg: 'updating client', name }); + public async updateClient(client: NewClient): Promise { + this.logger.info({ msg: 'updating client', name: client.name }); - this.logger.debug({ msg: 'updating client with following data', name, client }); + this.logger.debug({ msg: 'updating client with following data', name: client.name, client }); - const updatedClient = await this.clientRepository.updateAndReturn({ name, ...client }); + const updatedClients = await this.drizzle.update(clientTable).set(client).where(eq(clientTable.name, client.name)).returning(); this.logger.debug('client result returned from db'); - if (updatedClient === null) { + if (updatedClients.length === 0) { this.logger.debug('no rows were affected by client update command'); throw new ClientNotFoundError('client with given name was not found'); } - return updatedClient; + return updatedClients[0] as Client; } } diff --git a/apps/auth-manager/src/common/db/constants.ts b/apps/auth-manager/src/common/db/constants.ts index 44e09ca2..ae8ca99b 100644 --- a/apps/auth-manager/src/common/db/constants.ts +++ b/apps/auth-manager/src/common/db/constants.ts @@ -1,3 +1,4 @@ -export enum PgErrorCodes { - UNIQUE_VIOLATION = '23505', -} +export const pgErrorCodes = { + // eslint-disable-next-line @typescript-eslint/naming-convention + UNIQUE_VIOLATION: '23505', +}; diff --git a/apps/auth-manager/src/common/db/pagination.ts b/apps/auth-manager/src/common/db/pagination.ts index 4b5e4593..d6b4ace4 100644 --- a/apps/auth-manager/src/common/db/pagination.ts +++ b/apps/auth-manager/src/common/db/pagination.ts @@ -7,7 +7,7 @@ export interface PaginationParams { pageSize: number; } -export function paginationParamsToOffsetAndLimit(paginationParams?: PaginationParams): { limit?: number; offset?: number } { +export function paginationParamsToOffsetAndLimit(paginationParams?: PaginationParams): { limit: number; offset: number } { if (paginationParams === undefined) { return {}; } diff --git a/apps/auth-manager/src/common/db/utils.ts b/apps/auth-manager/src/common/db/utils.ts index 914a569a..cddb1880 100644 --- a/apps/auth-manager/src/common/db/utils.ts +++ b/apps/auth-manager/src/common/db/utils.ts @@ -1,15 +1,14 @@ -import type { FindOperator } from 'typeorm'; -import { Between, LessThan, MoreThan } from 'typeorm'; +import { between, lt, gt, type SQL, type Column } from 'drizzle-orm'; -export function createDatesComparison(earlyDate?: Date, laterDate?: Date): FindOperator | undefined { +export function createDatesComparison(column: Column, earlyDate?: Date, laterDate?: Date): SQL | undefined { if (earlyDate !== undefined && laterDate !== undefined) { - return Between(earlyDate, laterDate); + return between(column, earlyDate, laterDate); } if (earlyDate !== undefined) { - return MoreThan(earlyDate); + return gt(column, earlyDate); } if (laterDate !== undefined) { - return LessThan(laterDate); + return lt(column, laterDate); } return undefined; } diff --git a/apps/auth-manager/src/containerConfig.ts b/apps/auth-manager/src/containerConfig.ts index b411523a..8aca1b3c 100644 --- a/apps/auth-manager/src/containerConfig.ts +++ b/apps/auth-manager/src/containerConfig.ts @@ -6,7 +6,7 @@ import { jsLogger } from '@map-colonies/js-logger'; // import { DataSource } from 'typeorm'; import type { HealthCheck } from '@godaddy/terminus'; import { Pool } from 'pg'; -import { Bundle, createConnectionOptions, initConnection } from '@map-colonies/auth-core'; +import { Bundle, createConnectionOptions, createDrizzle, initConnection } from '@map-colonies/auth-core'; import { Registry } from 'prom-client'; import { DB_CONNECTION_TIMEOUT, SERVICES, SERVICE_NAME } from './common/constants'; import { domainRouterFactory, DOMAIN_ROUTER_SYMBOL } from './domain/routes/domainRouter'; @@ -67,6 +67,15 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise { token: SERVICES.TRACER, provider: { useValue: tracer } }, { token: SERVICES.METRICS, provider: { useValue: metricsRegistry } }, { token: Pool, provider: { useValue: pool } }, + { + token: SERVICES.DRIZZLE, + provider: { + useFactory: instanceCachingFactory((container) => { + const pool = container.resolve(Pool); + return createDrizzle(pool); + }), + }, + }, { token: SERVICES.HEALTHCHECK, provider: { @@ -80,27 +89,27 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise // token: SERVICES.DOMAIN_REPOSITORY, // provider: { useFactory: instanceCachingFactory(domainRepositoryFactory) }, // }, - // { token: DOMAIN_ROUTER_SYMBOL, provider: { useFactory: domainRouterFactory } }, + { token: DOMAIN_ROUTER_SYMBOL, provider: { useFactory: domainRouterFactory } }, // { // token: SERVICES.CLIENT_REPOSITORY, // provider: { useFactory: instanceCachingFactory(clientRepositoryFactory) }, // }, - // { token: CLIENT_ROUTER_SYMBOL, provider: { useFactory: clientRouterFactory } }, + { token: CLIENT_ROUTER_SYMBOL, provider: { useFactory: clientRouterFactory } }, // { // token: SERVICES.KEY_REPOSITORY, // provider: { useFactory: instanceCachingFactory(keyRepositoryFactory) }, // }, - // { token: KEY_ROUTER_SYMBOL, provider: { useFactory: keyRouterFactory } }, + { token: KEY_ROUTER_SYMBOL, provider: { useFactory: keyRouterFactory } }, // { // token: SERVICES.ASSET_REPOSITORY, // provider: { useFactory: instanceCachingFactory(assetRepositoryFactory) }, // }, - // { token: ASSET_ROUTER_SYMBOL, provider: { useFactory: assetRouterFactory } }, + { token: ASSET_ROUTER_SYMBOL, provider: { useFactory: assetRouterFactory } }, // { // token: SERVICES.CONNECTION_REPOSITORY, // provider: { useFactory: instanceCachingFactory(connectionRepositoryFactory) }, // }, - // { token: CONNECTION_ROUTER_SYMBOL, provider: { useFactory: connectionRouterFactory } }, + { token: CONNECTION_ROUTER_SYMBOL, provider: { useFactory: connectionRouterFactory } }, // { // token: SERVICES.BUNDLE_REPOSITORY, // provider: { diff --git a/apps/auth-manager/src/domain/models/domainManager.ts b/apps/auth-manager/src/domain/models/domainManager.ts index eb593149..91009b3f 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -38,7 +38,7 @@ export class DomainManager { public async createDomain(domain: NewDomain): Promise { this.logger.info({ msg: 'creating domain', name: domain.name }); try { - await this.domainRepository.insert(domain); + await this.drizzle.insert(domainTable).values(domain); return domain; } catch (error) { if (error instanceof Error && error.message.includes('duplicate key value violates unique constraint')) { diff --git a/apps/auth-manager/src/utils/mapper.ts b/apps/auth-manager/src/utils/mapper.ts new file mode 100644 index 00000000..c485599a --- /dev/null +++ b/apps/auth-manager/src/utils/mapper.ts @@ -0,0 +1,22 @@ +export type ShallowRemoveNulls = { + [K in keyof T as null extends T[K] ? never : K]: T[K]; +} & { + [K in keyof T as null extends T[K] ? K : never]?: Exclude; +} extends infer O + ? { [K in keyof O]: O[K] } + : never; + +export function removeNulls(obj: T): ShallowRemoveNulls { + const result: Record = {}; + + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + const value = obj[key]; + if (value !== null) { + result[key] = value; + } + } + } + + return result as ShallowRemoveNulls; +} diff --git a/apps/auth-manager/tests/integration/domain/domain.spec.mts b/apps/auth-manager/tests/integration/domain/domain.spec.mts index fad73e13..72fc04df 100644 --- a/apps/auth-manager/tests/integration/domain/domain.spec.mts +++ b/apps/auth-manager/tests/integration/domain/domain.spec.mts @@ -3,11 +3,11 @@ import { jsLogger } from '@map-colonies/js-logger'; import { trace } from '@opentelemetry/api'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; -import { DataSource } from 'typeorm'; import { faker } from '@faker-js/faker'; import 'jest-openapi'; -import type { IDomain } from '@map-colonies/auth-core'; -import { Domain } from '@map-colonies/auth-core'; +import { Pool } from 'pg'; +import type { Drizzle } from '@map-colonies/auth-core'; +import { domainTable } from '@map-colonies/auth-core'; import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; import type { paths, operations } from 'auth-openapi'; @@ -35,14 +35,14 @@ describe('domain', function () { }); afterAll(async function () { - await depContainer.resolve(DataSource).destroy(); + await depContainer.resolve(Pool).end(); }); describe('Happy Path', function () { describe('GET /domain', function () { it('should return 200 status code and a list of domains', async function () { - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Domain).save([{ name: 'avi' }, { name: 'iva' }]); + const drizzle = depContainer.resolve(SERVICES.DRIZZLE); + await drizzle.insert(domainTable).values([{ name: 'avi' }, { name: 'iva' }]); const res = await requestSender.getDomains(); @@ -108,7 +108,7 @@ describe('domain', function () { }); describe('Sad Path', function () { - const MockProvider = { insert: vi.fn(), find: vi.fn() }; + const MockProvider = { select: vi.fn(), insert: vi.fn() }; let mockedSender: RequestSender; beforeEach(async function () { @@ -116,18 +116,18 @@ describe('domain', function () { override: [ { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - { token: SERVICES.DOMAIN_REPOSITORY, provider: { useValue: MockProvider } }, + { token: SERVICES.DRIZZLE, provider: { useValue: MockProvider } }, ], useChild: true, }); mockedSender = await createRequestSender(OPENAPI_PATH, app); - await container.resolve(DataSource).destroy(); + await container.resolve(Pool).end(); vi.resetAllMocks(); }); describe('GET /domain', function () { it('should return 500 status code if db throws an error', async function () { - MockProvider.find.mockRejectedValue(new Error('')); + MockProvider.select.mockRejectedValue(new Error('')); const res = await mockedSender.getDomains(); diff --git a/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts b/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts index ebe61871..794d78af 100644 --- a/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts +++ b/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts @@ -1,9 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; -import { Environment } from '@map-colonies/auth-core'; import { getFakeAsset } from 'test-utils'; import { AssetManager } from '@src/asset/models/assetManager.js'; -import { AssetNotFoundError, AssetVersionMismatchError } from '@src/asset/models/errors.js'; +import { AssetVersionMismatchError } from '@src/asset/models/errors.js'; import type { AssetRepository } from '@src/asset/DAL/assetRepository.js'; const logger = await jsLogger({ enabled: false }); @@ -21,71 +20,6 @@ describe('AssetManager', () => { vi.resetAllMocks(); }); - describe('#getAssets', () => { - it('should return the array of assets', async function () { - const asset = getFakeAsset(); - mockedRepository.findBy.mockResolvedValue([asset]); - - const assetPromise = assetManager.getAssets({}); - - await expect(assetPromise).resolves.toStrictEqual([asset]); - }); - - it('should throw an error if one is thrown by the repository', async function () { - mockedRepository.findBy.mockRejectedValue(new Error()); - - const assetPromise = assetManager.getAssets({}); - - await expect(assetPromise).rejects.toThrow(); - }); - }); - - describe('#getNamedAssets', () => { - it('should return the array of assets', async function () { - const asset = getFakeAsset(); - mockedRepository.findBy.mockResolvedValue([asset]); - - const assetPromise = assetManager.getNamedAssets('avi'); - - await expect(assetPromise).resolves.toStrictEqual([asset]); - }); - - it('should throw an error if one is thrown by the repository', async function () { - mockedRepository.findBy.mockRejectedValue(new Error()); - - const assetPromise = assetManager.getNamedAssets('avi'); - - await expect(assetPromise).rejects.toThrow(); - }); - }); - - describe('#getAsset', () => { - it('should return the asset', async function () { - const asset = getFakeAsset(); - mockedRepository.findOne.mockResolvedValue(asset); - - const assetPromise = assetManager.getAsset(Environment.STAGE, 1); - - await expect(assetPromise).resolves.toStrictEqual(asset); - }); - - it('should throw an error if one is thrown by the repository', async function () { - mockedRepository.findOne.mockRejectedValue(new Error()); - - const assetPromise = assetManager.getAsset(Environment.STAGE, 1); - - await expect(assetPromise).rejects.toThrow(); - }); - - it('should throw an error if the asset already exists', async function () { - mockedRepository.findOne.mockResolvedValue(null); - - const assetPromise = assetManager.getAsset(Environment.STAGE, 1); - - await expect(assetPromise).rejects.toThrow(AssetNotFoundError); - }); - }); - describe('#upsertAsset', () => { let manager: AssetManager; const transactionRepo = { @@ -103,18 +37,6 @@ describe('AssetManager', () => { manager = new AssetManager(logger, repo as unknown as AssetRepository); }); - it("should insert the asset and return it if it doesn't exist in the database", async () => { - const asset = getFakeAsset(); - transactionRepo.getMaxVersionWithLock.mockResolvedValue(null); - transactionRepo.save.mockResolvedValue(asset); - - const assetPromise = manager.upsertAsset(asset); - - await expect(assetPromise).resolves.toStrictEqual(asset); - expect(transactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(transactionRepo.save).toHaveBeenCalledTimes(1); - }); - it('should update the asset,return it, and advance the version by 1 if it exist in the database and the version matches', async () => { const asset = getFakeAsset(); asset.version = 2; diff --git a/apps/auth-manager/tests/unit/bundle/models/bundleModel.spec.mts b/apps/auth-manager/tests/unit/bundle/models/bundleModel.spec.mts deleted file mode 100644 index 7f503e29..00000000 --- a/apps/auth-manager/tests/unit/bundle/models/bundleModel.spec.mts +++ /dev/null @@ -1,68 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import type { Bundle } from '@map-colonies/auth-core'; -import type { Repository } from 'typeorm'; -import { getFakeBundle } from 'test-utils'; -import { BundleManager } from '@src/bundle/models/bundleManager.js'; -import { BundleNotFoundError } from '@src/bundle/models/errors.js'; - -const logger = await jsLogger({ enabled: false }); - -describe('BundleManager', () => { - let bundleManager: BundleManager; - const mockedRepository = { - findBy: vi.fn(), - findOneBy: vi.fn(), - }; - - beforeEach(function () { - bundleManager = new BundleManager(logger, mockedRepository as unknown as Repository); - vi.resetAllMocks(); - }); - - describe('#getBundles', () => { - it('should return the array of bundles', async function () { - const bundle = getFakeBundle(); - mockedRepository.findBy.mockResolvedValue([bundle]); - - const bundlePromise = bundleManager.getBundles({}); - - await expect(bundlePromise).resolves.toStrictEqual([bundle]); - }); - - it('should throw an error if thrown by the ORM', async function () { - mockedRepository.findBy.mockRejectedValue(new Error()); - - const bundlePromise = bundleManager.getBundles({}); - - await expect(bundlePromise).rejects.toThrow(); - }); - }); - - describe('#getBundle', () => { - it('should return the bundle', async function () { - const bundle = getFakeBundle(); - mockedRepository.findOneBy.mockResolvedValue(bundle); - - const bundlePromise = bundleManager.getBundle(1); - - await expect(bundlePromise).resolves.toStrictEqual(bundle); - }); - - it('should throw NotFoundError if the bundle is not in the db', async function () { - mockedRepository.findOneBy.mockResolvedValue(null); - - const bundlePromise = bundleManager.getBundle(1); - - await expect(bundlePromise).rejects.toThrow(BundleNotFoundError); - }); - - it('should throw an error if the db throws one', async function () { - mockedRepository.findOneBy.mockRejectedValue(new Error()); - - const bundlePromise = bundleManager.getBundle(1); - - await expect(bundlePromise).rejects.toThrow(); - }); - }); -}); diff --git a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts index ed3d2118..1ba24400 100644 --- a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts +++ b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts @@ -24,63 +24,7 @@ describe('ClientManager', () => { vi.resetAllMocks(); }); - describe('#getClients', () => { - it('should return the array of clients', async function () { - const client = getFakeClient(true); - mockedRepository.findAndCount.mockResolvedValue([[client], 1]); - - const domainPromise = clientManager.getClients({}); - - await expect(domainPromise).resolves.toStrictEqual([[client], 1]); - }); - - it('should throw an error if thrown by the ORM', async function () { - mockedRepository.findAndCount.mockRejectedValue(new Error()); - - const domainPromise = clientManager.getClients({}); - - await expect(domainPromise).rejects.toThrow(); - }); - }); - - describe('#getClient', () => { - it('should return the client', async function () { - const client = getFakeClient(true); - mockedRepository.findOne.mockResolvedValue(client); - - const clientPromise = clientManager.getClient(client.name); - - await expect(clientPromise).resolves.toStrictEqual(client); - }); - - it('should throw an error if thrown by the ORM', async function () { - mockedRepository.findOne.mockRejectedValue(new Error()); - - const clientPromise = clientManager.getClient('avi'); - - await expect(clientPromise).rejects.toThrow(); - }); - - it('should return client not found error', async function () { - mockedRepository.findOne.mockResolvedValue(null); - - const clientPromise = clientManager.getClient('avi'); - - await expect(clientPromise).rejects.toThrow(ClientNotFoundError); - }); - }); - describe('#createClient', () => { - it('should insert into the db and return the client', async function () { - const client = getFakeClient(false); - mockedRepository.insert.mockResolvedValue(undefined); - - const domainPromise = clientManager.createClient(client); - - await expect(domainPromise).resolves.toStrictEqual(client); - expect(mockedRepository.insert).toHaveBeenCalled(); - }); - it('should throw AlreadyExistsError if the client is already in', async function () { const client = getFakeClient(false); const dbError = new DatabaseError('avi', 5, 'error'); @@ -92,47 +36,5 @@ describe('ClientManager', () => { await expect(domainPromise).rejects.toThrow(ClientAlreadyExistsError); expect(mockedRepository.insert).toHaveBeenCalled(); }); - - it('should throw an error if the db throws one', async function () { - const client = getFakeClient(false); - mockedRepository.insert.mockRejectedValue(new Error()); - - const domainPromise = clientManager.createClient(client); - - await expect(domainPromise).rejects.toThrow(); - expect(mockedRepository.insert).toHaveBeenCalled(); - }); - }); - - describe('#updateClient', () => { - it('should update the db and return the client', async function () { - const { name, ...client } = getFakeClient(false); - mockedRepository.updateAndReturn.mockResolvedValue({ name, ...client }); - - const domainPromise = clientManager.updateClient(name, client); - - await expect(domainPromise).resolves.toStrictEqual({ name, ...client }); - expect(mockedRepository.updateAndReturn).toHaveBeenCalled(); - }); - - it('should throw ClientNotFoundError if the client is not found', async function () { - const { name, ...client } = getFakeClient(false); - mockedRepository.updateAndReturn.mockResolvedValue(null); - - const domainPromise = clientManager.updateClient(name, client); - - await expect(domainPromise).rejects.toThrow(ClientNotFoundError); - expect(mockedRepository.updateAndReturn).toHaveBeenCalled(); - }); - - it('should throw an error if the db throws one', async function () { - const { name, ...client } = getFakeClient(false); - mockedRepository.updateAndReturn.mockRejectedValue(new Error()); - - const domainPromise = clientManager.updateClient(name, client); - - await expect(domainPromise).rejects.toThrow(); - expect(mockedRepository.updateAndReturn).toHaveBeenCalled(); - }); }); }); diff --git a/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts b/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts index b5f7652f..de1390ba 100644 --- a/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts +++ b/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts @@ -34,43 +34,6 @@ describe('ConnectionManager', () => { vi.resetAllMocks(); }); - describe('#getConnections', () => { - it('should throw an error if one is thrown by the repository', async function () { - mockedConnectionRepository.findAndCount.mockRejectedValue(new Error()); - - const connectionPromise = connectionManager.getConnections({}); - - await expect(connectionPromise).rejects.toThrow(); - }); - }); - - describe('#getConnection', () => { - it('should return the connection', async function () { - const connection = getFakeConnection(); - mockedConnectionRepository.findOne.mockResolvedValue(connection); - - const connectionPromise = connectionManager.getConnection('avi', Environment.STAGE, 1); - - await expect(connectionPromise).resolves.toStrictEqual(connection); - }); - - it('should throw an error if one is thrown by the repository', async function () { - mockedConnectionRepository.findOne.mockRejectedValue(new Error()); - - const connectionPromise = connectionManager.getConnection('avi', Environment.STAGE, 1); - - await expect(connectionPromise).rejects.toThrow(); - }); - - it("should throw an error if the connection doesn't exists", async function () { - mockedConnectionRepository.findOne.mockResolvedValue(null); - - const connectionPromise = connectionManager.getConnection('avi', Environment.STAGE, 1); - - await expect(connectionPromise).rejects.toThrow(ConnectionNotFoundError); - }); - }); - describe('#upsertConnection', () => { let manager: ConnectionManager; const connectionTransactionRepo = { @@ -120,18 +83,6 @@ describe('ConnectionManager', () => { ); }); - it("should insert the connection and return it if it doesn't exist in the database", async () => { - const connection = getFakeConnection(); - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(null); - connectionTransactionRepo.save.mockResolvedValue(connection); - - const connectionPromise = manager.upsertConnection(connection); - - await expect(connectionPromise).resolves.toStrictEqual(connection); - expect(connectionTransactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(connectionTransactionRepo.save).toHaveBeenCalledTimes(1); - }); - it('should update the connection,return it, and advance the version by 1 if it exist in the database and the version matches', async () => { const connection = getFakeConnection(); connection.version = 2; @@ -146,31 +97,6 @@ describe('ConnectionManager', () => { expect(connectionTransactionRepo.save).toHaveBeenCalledWith(connection); }); - it('should generate a token if the token is an empty string', async () => { - const keys = getRealKeys(); - const connection = getFakeConnection(); - connection.token = ''; - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - connectionTransactionRepo.save.mockResolvedValue(connection); - keyTransactionRepo.getLatestKeys = vi.fn().mockResolvedValue([{ privateKey: keys[0], environment: connection.environment }]); - - const connectionRes = await manager.upsertConnection({ ...connection, version: 1 }); - - expect(connectionRes).not.toBe(''); - }); - - it('should return the connection with empty token if the token is an empty string and ignoreTokenErrors is true', async () => { - const connection = getFakeConnection(); - connection.token = ''; - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - connectionTransactionRepo.save.mockResolvedValue(connection); - keyTransactionRepo.getLatestKeys = vi.fn().mockResolvedValue([]); - - const connectionRes = await manager.upsertConnection({ ...connection, version: 1 }, true); - - expect(connectionRes).toHaveProperty('token', ''); - }); - it('should return the connection with empty token if the token generation failed and ignoreTokenErrors is true', async () => { const connection = getFakeConnection(); connection.token = ''; @@ -182,71 +108,5 @@ describe('ConnectionManager', () => { expect(connectionRes).toHaveProperty('token', ''); }); - - it('should throw an error if the token is an empty string and a key is not found', async () => { - const connection = getFakeConnection(); - connection.token = ''; - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - connectionTransactionRepo.save.mockResolvedValue(connection); - keyTransactionRepo.getLatestKeys = vi.fn().mockResolvedValue([]); - - const connectionPromise = manager.upsertConnection({ ...connection, version: 1 }); - - await expect(connectionPromise).rejects.toThrow(KeyNotFoundError); - }); - - it('should throw an error if the token generation failed', async () => { - const connection = getFakeConnection(); - connection.token = ''; - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - connectionTransactionRepo.save.mockResolvedValue(connection); - keyTransactionRepo.getLatestKeys = vi.fn().mockResolvedValue([{ environment: connection.environment, privateKey: 'avi' }]); - - const connectionPromise = manager.upsertConnection({ ...connection, version: 1 }); - - await expect(connectionPromise).rejects.toThrow(); - }); - - it('should throw an error if a client with the given name do not exist', async () => { - const connection = getFakeConnection(); - clientTransactionRepo.findOneBy.mockResolvedValue(null); - - const connectionPromise = manager.upsertConnection({ ...connection, version: 2 }); - - await expect(connectionPromise).rejects.toThrow(ClientNotFoundError); - expect(connectionTransactionRepo.save).not.toHaveBeenCalled(); - }); - - it("should throw an error if a domain list contains a domain that doesn't exist", async () => { - const connection = getFakeConnection(); - domainTransactionRepo.checkInputForNonExistingDomains.mockResolvedValue(['avi']); - - const connectionPromise = manager.upsertConnection({ ...connection, version: 2 }); - - await expect(connectionPromise).rejects.toThrow(DomainNotFoundError); - expect(connectionTransactionRepo.save).not.toHaveBeenCalled(); - }); - - it("should throw an error if a connection doesn't exist and the version supplied is not 1", async () => { - const connection = getFakeConnection(); - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(null); - - const connectionPromise = manager.upsertConnection({ ...connection, version: 2 }); - - await expect(connectionPromise).rejects.toThrow(ConnectionVersionMismatchError); - expect(connectionTransactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(connectionTransactionRepo.save).not.toHaveBeenCalled(); - }); - - it("should throw an error if a connection exist but the supplied version doesn't match database version", async () => { - const connection = getFakeConnection(); - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - - const connectionPromise = manager.upsertConnection({ ...connection, version: 2 }); - - await expect(connectionPromise).rejects.toThrow(ConnectionVersionMismatchError); - expect(connectionTransactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(connectionTransactionRepo.save).not.toHaveBeenCalled(); - }); }); }); diff --git a/apps/auth-manager/tests/unit/domain/models/domainModel.spec.mts b/apps/auth-manager/tests/unit/domain/models/domainModel.spec.mts deleted file mode 100644 index bfaead8d..00000000 --- a/apps/auth-manager/tests/unit/domain/models/domainModel.spec.mts +++ /dev/null @@ -1,67 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import type { DomainRepository } from '@src/domain/DAL/domainRepository.js'; -import { DomainManager } from '@src/domain/models/domainManager.js'; -import { DomainAlreadyExistsError } from '@src/domain/models/errors.js'; - -const logger = await jsLogger({ enabled: false }); - -describe('DomainManager', () => { - let domainManager: DomainManager; - const mockedRepository = { - findAndCount: vi.fn(), - insert: vi.fn(), - }; - - beforeEach(function () { - domainManager = new DomainManager(logger, mockedRepository as unknown as DomainRepository); - vi.resetAllMocks(); - }); - - describe('#getDomains', () => { - it('should return the array of domains', async function () { - mockedRepository.findAndCount.mockResolvedValue([{ name: 'avi' }]); - - const domainPromise = domainManager.getDomains(); - - await expect(domainPromise).resolves.toStrictEqual([{ name: 'avi' }]); - }); - - it('should throw an error if thrown by the ORM', async function () { - mockedRepository.findAndCount.mockRejectedValue(new Error()); - - const domainPromise = domainManager.getDomains(); - - await expect(domainPromise).rejects.toThrow(); - }); - }); - - describe('#createDomain', () => { - it('should insert into the db and return the domain', async function () { - mockedRepository.insert.mockResolvedValue(undefined); - - const domainPromise = domainManager.createDomain({ name: 'avi' }); - - await expect(domainPromise).resolves.toStrictEqual({ name: 'avi' }); - expect(mockedRepository.insert).toHaveBeenCalled(); - }); - - it('should throw AlreadyExistsError if the domain is already in', async function () { - mockedRepository.insert.mockRejectedValue(new Error('duplicate key value violates unique constraint')); - - const domainPromise = domainManager.createDomain({ name: 'avi' }); - - await expect(domainPromise).rejects.toThrow(DomainAlreadyExistsError); - expect(mockedRepository.insert).toHaveBeenCalled(); - }); - - it('should throw an error if the db throws one', async function () { - mockedRepository.insert.mockRejectedValue(new Error()); - - const domainPromise = domainManager.createDomain({ name: 'avi' }); - - await expect(domainPromise).rejects.toThrow(); - expect(mockedRepository.insert).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/auth-core/src/db/entities/client.ts b/packages/auth-core/src/db/entities/client.ts index 76c8d016..3fa5a929 100644 --- a/packages/auth-core/src/db/entities/client.ts +++ b/packages/auth-core/src/db/entities/client.ts @@ -2,8 +2,8 @@ // import type { IClient, PointOfContact } from '../../model'; import { json, text, timestamp } from 'drizzle-orm/pg-core'; +import type { PointOfContact } from '../../model'; import { authManagerSchema, createdAtColumn } from './common'; -import { PointOfContact } from '../../model'; // /** // * The typeorm implementation of the IClient interface. @@ -44,7 +44,7 @@ export const clientTable = authManagerSchema.table('client', { description: text(), branch: text(), createdAt: createdAtColumn, - updateAt: timestamp({ withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('update_at', { withTimezone: true }).defaultNow().notNull(), techPointOfContact: json('tech_point_of_contact').$type(), productPointOfContact: json('product_point_of_contact').$type(), tags: text().array(), diff --git a/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql b/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql index ab413f43..772bca8c 100644 --- a/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql +++ b/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql @@ -1,4 +1,4 @@ -CREATE SCHEMA "auth_manager"; +CREATE SCHEMA IF NOT EXISTS "auth_manager" ; --> statement-breakpoint CREATE TYPE "auth_manager"."asset_type_enum" AS ENUM('TEST', 'TEST_DATA', 'POLICY', 'DATA');--> statement-breakpoint CREATE TYPE "auth_manager"."environment_enum" AS ENUM('np', 'stage', 'prod');--> statement-breakpoint diff --git a/packages/auth-core/src/db/utils/createConnection.ts b/packages/auth-core/src/db/utils/createConnection.ts index f38ac6d2..75dd310b 100644 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ b/packages/auth-core/src/db/utils/createConnection.ts @@ -8,14 +8,15 @@ import { migrate } from 'drizzle-orm/node-postgres/migrator'; import { relations } from '../entities'; export function createConnectionOptions(dbOptions: commonDbFullV1Type): PoolConfig { - const { ssl } = dbOptions; - const dbConfig: PoolConfig = structuredClone(dbOptions); + const { ssl, ...rest } = dbOptions; + const dbConfig: PoolConfig = structuredClone(rest); dbConfig.application_name = `${hostname()}-${process.env.NODE_ENV ?? 'unknown_env'}`; dbConfig.user = dbOptions.username; if (ssl.enabled) { dbConfig.password = undefined; dbConfig.ssl = { key: readFileSync(ssl.key), cert: readFileSync(ssl.cert), ca: readFileSync(ssl.ca) }; } + return dbConfig; } @@ -32,8 +33,7 @@ export function createDrizzle(pool: Pool): Drizzle { } export async function runMigrations(drizzle: NodePgDatabase): Promise { - const optionalFolders = ['./db/migrations', './src/db/migrations', './migrations']; - console.log(__dirname); + const optionalFolders = ['./db/migrations', './src/db/migrations', './migrations'].map((folder) => path.join(__dirname, '..', '..', '..', folder)); let migrationsFolder = null; for (const folder of optionalFolders) { diff --git a/packages/auth-core/src/model/asset.ts b/packages/auth-core/src/model/asset.ts index fd3e5205..24098a8c 100644 --- a/packages/auth-core/src/model/asset.ts +++ b/packages/auth-core/src/model/asset.ts @@ -1,18 +1,19 @@ // import type { Environments } from './common'; -// /* eslint-disable @typescript-eslint/naming-convention */ -// export const AssetType = { -// /** OPA test files. */ -// TEST: 'TEST', -// TEST_DATA: 'TEST_DATA', -// /** OPA policy files. */ -// POLICY: 'POLICY', -// /** OPA data files, name should end with .json or .yaml. */ -// DATA: 'DATA', -// } as const; -// /* eslint-enable @typescript-eslint/naming-convention */ +import type { assetTypeEnum } from '../db'; -// export type AssetTypes = (typeof AssetType)[keyof typeof AssetType]; +/* eslint-disable @typescript-eslint/naming-convention */ +type assetTypeEnumValues = typeof assetTypeEnum.enumValues; +export const AssetType: { [K in assetTypeEnumValues[number]]: K } = { + /** OPA test files. */ + TEST: 'TEST', + TEST_DATA: 'TEST_DATA', + /** OPA policy files. */ + POLICY: 'POLICY', + /** OPA data files, name should end with .json or .yaml. */ + DATA: 'DATA', +} as const; +/* eslint-enable @typescript-eslint/naming-convention */ // /** // * Describes the metadata and content of assets - files that will be part of the bundle. diff --git a/packages/auth-core/src/model/common.ts b/packages/auth-core/src/model/common.ts index 73d6099c..1e903f50 100644 --- a/packages/auth-core/src/model/common.ts +++ b/packages/auth-core/src/model/common.ts @@ -1,13 +1,16 @@ +import type { environmentEnum } from '../db'; + +type EnvironmentValues = typeof environmentEnum.enumValues; /** The possible authentication deployment environments. */ /* eslint-disable @typescript-eslint/naming-convention */ -// export const Environment = { -// /** Non production, may also be called dev. */ -// NP: 'np', -// /** The staging environment, may also be called integration. */ -// STAGE: 'stage', -// /** The production environment. */ -// PRODUCTION: 'prod', -// } as const; -// /* eslint-enable @typescript-eslint/naming-convention */ +export const Environment: { [K in EnvironmentValues[number] as Uppercase]: K } = { + /** Non production, may also be called dev. */ + NP: 'np', + /** The staging environment, may also be called integration. */ + STAGE: 'stage', + /** The production environment. */ + PROD: 'prod', +} as const; +/* eslint-enable @typescript-eslint/naming-convention */ // export type Environments = (typeof Environment)[keyof typeof Environment]; diff --git a/packages/test-utils/src/drizzle.ts b/packages/test-utils/src/drizzle.ts index 0d5bd001..05e9b166 100644 --- a/packages/test-utils/src/drizzle.ts +++ b/packages/test-utils/src/drizzle.ts @@ -7,7 +7,7 @@ import type { commonDbFullV1Type } from '@map-colonies/schemas'; */ export async function resetAndMigrate(connection: Pool): Promise { await connection.query(`DROP SCHEMA IF EXISTS "${authManagerSchema.schemaName}" CASCADE`); - await connection.query(`CREATE SCHEMA "${authManagerSchema.schemaName}"`); + // await connection.query(`CREATE SCHEMA "${authManagerSchema.schemaName}"`); const db = createDrizzle(connection); await runMigrations(db); diff --git a/packages/test-utils/src/fakers.ts b/packages/test-utils/src/fakers.ts index cab27930..db3a952f 100644 --- a/packages/test-utils/src/fakers.ts +++ b/packages/test-utils/src/fakers.ts @@ -1,11 +1,24 @@ import { faker } from '@faker-js/faker'; -import type { Connection, IAsset, IBundle, IClient, IConnection, JWKPrivateKey, JWKPublicKey } from '@map-colonies/auth-core'; +import type { + NewAsset, + Bundle, + Client, + Connection, + JWKPrivateKey, + JWKPublicKey, + Asset, + NewConnection, + NewBundle, + NewClient, +} from '@map-colonies/auth-core'; import { AssetType, Environment } from '@map-colonies/auth-core'; const EIGHT = 8; const THREE = 3; -export function getFakeAsset(includeCreated?: boolean): IAsset { +export function getFakeAsset(includeCreated: true): Asset; +export function getFakeAsset(includeCreated?: false): NewAsset; +export function getFakeAsset(includeCreated?: boolean): Asset | NewAsset { return { createdAt: includeCreated === true ? faker.date.past() : undefined, environment: [Environment.NP], @@ -13,14 +26,16 @@ export function getFakeAsset(includeCreated?: boolean): IAsset { name: faker.string.alpha(EIGHT), type: faker.helpers.arrayElement(Object.values(AssetType)), uri: faker.system.filePath(), - value: Buffer.from(faker.lorem.paragraph()).toString('base64'), + value: Buffer.from(faker.lorem.paragraph()), version: 1, }; } -export function getFakeConnection(): Connection { +export function getFakeConnection(includeCreated: true): Connection; +export function getFakeConnection(includeCreated?: false): NewConnection; +export function getFakeConnection(includeCreated?: boolean): Connection | NewConnection { return { - createdAt: faker.date.past(), + createdAt: includeCreated === true ? faker.date.past() : undefined, environment: Environment.NP, version: 1, name: faker.string.alpha(EIGHT), @@ -33,13 +48,9 @@ export function getFakeConnection(): Connection { }; } -export function getFakeIConnection(includeCreated?: boolean): IConnection { - const connection: IConnection = getFakeConnection(); - connection.createdAt = includeCreated === true ? faker.date.past() : undefined; - return connection; -} - -export function getFakeBundle(includeCreated?: boolean): IBundle { +export function getFakeBundle(includeCreated: true): Bundle; +export function getFakeBundle(includeCreated?: false): NewBundle; +export function getFakeBundle(includeCreated?: boolean): Bundle | NewBundle { return { id: includeCreated === true ? faker.number.int() : undefined, hash: faker.string.alpha(EIGHT), @@ -53,10 +64,12 @@ export function getFakeBundle(includeCreated?: boolean): IBundle { }; } -export function getFakeClient(includeGeneratedFields: boolean): IClient { +export function getFakeClient(includeGeneratedFields: true): Client; +export function getFakeClient(includeGeneratedFields?: false): NewClient; +export function getFakeClient(includeGeneratedFields?: boolean): NewClient | Client { const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); - const client: IClient = { + const client: NewClient = { name: faker.internet.username({ firstName, lastName }), hebName: 'אבי', branch: faker.company.buzzVerb(), @@ -74,7 +87,7 @@ export function getFakeClient(includeGeneratedFields: boolean): IClient { tags: [faker.word.adjective(), faker.company.buzzNoun()], }; - if (includeGeneratedFields) { + if (includeGeneratedFields === true) { client.createdAt = faker.date.past(); client.updatedAt = faker.date.between({ from: client.createdAt, to: Date.now() }); } From e143001b44bb5ce9de021233992be44d5aecbd42 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 20 May 2026 16:46:52 +0300 Subject: [PATCH 06/14] chore(global): progress --- apps/auth-manager/package.json | 4 - .../src/asset/DAL/assetRepository.ts | 8 +- .../src/client/models/clientManager.ts | 30 +- apps/auth-manager/src/common/db/utils.ts | 18 +- .../connection/DAL/connectionRepository.ts | 76 ++--- .../controllers/connectionController.ts | 8 +- .../connection/models/connectionManager.ts | 188 ++++++------ .../src/domain/DAL/domainRepository.ts | 44 +-- .../auth-manager/src/key/DAL/keyRepository.ts | 127 ++++---- .../auth-manager/src/key/models/keyManager.ts | 67 ++-- .../configurations/vitest.globalSetup.mts | 2 +- .../tests/integration/asset/asset.spec.mts | 138 ++++----- .../tests/integration/bundle/bundle.spec.mts | 74 ++--- .../tests/integration/client/client.spec.mts | 227 ++++++-------- .../connection/connection.spec.mts | 286 +++++++++--------- .../tests/integration/domain/domain.spec.mts | 34 +-- apps/auth-manager/tests/integration/setup.ts | 35 +++ packages/auth-core/src/db/entities/common.ts | 2 + .../src/db/utils/createConnection.ts | 2 + packages/test-utils/src/fakers.ts | 17 +- 20 files changed, 675 insertions(+), 712 deletions(-) create mode 100644 apps/auth-manager/tests/integration/setup.ts diff --git a/apps/auth-manager/package.json b/apps/auth-manager/package.json index 41f9acaa..5fdc68a1 100644 --- a/apps/auth-manager/package.json +++ b/apps/auth-manager/package.json @@ -19,10 +19,6 @@ "test": "vitest run", "test:watch": "vitest watch", "test:ui": "vitest --ui", - "typeorm": "node ../../node_modules/typeorm/cli.js -d ./dataSource.mjs", - "migration:create": "npm run typeorm migration:generate --", - "migration:run": "npm run typeorm migration:run -- ", - "migration:revert": "npm run typeorm migration:revert -- ", "lint": "eslint .", "lint:fix": "eslint --fix .", "prebuild": "npm run clean", diff --git a/apps/auth-manager/src/asset/DAL/assetRepository.ts b/apps/auth-manager/src/asset/DAL/assetRepository.ts index b941ea94..80017d34 100644 --- a/apps/auth-manager/src/asset/DAL/assetRepository.ts +++ b/apps/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,11 +1,9 @@ import { and, eq, max } from 'drizzle-orm'; -import { assetTable, type Drizzle } from '@map-colonies/auth-core'; -import { inject, injectable } from 'tsyringe'; +import { assetTable, type DrizzleTx, type Drizzle } from '@map-colonies/auth-core'; +import { inject, injectable, Lifecycle, scoped } from 'tsyringe'; import { SERVICES } from '@common/constants'; -type DrizzleTx = Parameters[0]>[0]; - -@injectable() +@scoped(Lifecycle.ContainerScoped) export class AssetRepository { public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} diff --git a/apps/auth-manager/src/client/models/clientManager.ts b/apps/auth-manager/src/client/models/clientManager.ts index 07d167e5..8c6d6e93 100644 --- a/apps/auth-manager/src/client/models/clientManager.ts +++ b/apps/auth-manager/src/client/models/clientManager.ts @@ -2,18 +2,19 @@ import { type Logger } from '@map-colonies/js-logger'; import { inject, injectable } from 'tsyringe'; // import { ArrayContains, ILike, QueryFailedError } from 'typeorm'; import { DatabaseError } from 'pg'; -import { count, DrizzleQueryError, eq, and, arrayContains, between } from 'drizzle-orm'; +import { count, DrizzleQueryError, eq, and, arrayContains, asc, desc, SQL, AnyColumn, ilike } from 'drizzle-orm'; import { clientTable, type Client, type Drizzle, type NewClient } from '@map-colonies/auth-core'; +import { PgTable } from 'drizzle-orm/pg-core'; import { SERVICES } from '@common/constants'; import { pgErrorCodes } from '@common/db/constants'; -import { createDatesComparison } from '@common/db/utils'; +import { createDatesComparison, sortOptionsToOrderBy } from '@common/db/utils'; import { SortOptions } from '@src/common/db/sort'; -import { PaginationParams, paginationParamsToFindOptions, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; +import { PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import { ClientAlreadyExistsError, ClientNotFoundError } from './errors'; import { ClientSearchParams } from './client'; -function isQueryFailedError(err: unknown): err is DrizzleQueryError { - return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'QueryFailedError'; +function isDrizzleQueryError(err: unknown): err is DrizzleQueryError { + return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'DrizzleQueryError'; } @injectable() @@ -63,7 +64,7 @@ export class ClientManager { // }; const whereClause = and( - name !== undefined ? eq(clientTable.name, name) : undefined, + name !== undefined ? ilike(clientTable.name, `%${name}%`) : undefined, branch !== undefined ? eq(clientTable.branch, branch) : undefined, tags !== undefined ? arrayContains(clientTable.tags, tags) : undefined, createDatesComparison(clientTable.createdAt, createdAfter, createdBefore), @@ -72,9 +73,19 @@ export class ClientManager { const { limit, offset } = paginationParamsToOffsetAndLimit(paginationParams); - const clients = await this.drizzle.select().from(clientTable).where(whereClause).limit(limit).offset(offset).orderBy(sortParams); + const clientsQuery = this.drizzle + .select() + .from(clientTable) + .where(whereClause) + .limit(limit) + .offset(offset) + .orderBy(...sortOptionsToOrderBy(clientTable, sortParams ?? {})); - const countResult = await this.drizzle.select({ count: count() }).from(clientTable).where(whereClause); + const countQuery = this.drizzle.select({ count: count() }).from(clientTable).where(whereClause); + + const [clients, countResult] = await Promise.all([clientsQuery, countQuery]); + + return [clients, countResult[0]?.count ?? 0]; } public async getClient(name: string): Promise { @@ -101,7 +112,8 @@ export class ClientManager { return res[0] as Client; } catch (error) { - if (isQueryFailedError(error) && error.cause instanceof DatabaseError && error.cause.code === pgErrorCodes.UNIQUE_VIOLATION) { + console.error(error); + if (isDrizzleQueryError(error) && error.cause instanceof DatabaseError && error.cause.code === pgErrorCodes.UNIQUE_VIOLATION) { throw new ClientAlreadyExistsError('client already exists'); } this.logger.debug('create client throw an unrecognized error'); diff --git a/apps/auth-manager/src/common/db/utils.ts b/apps/auth-manager/src/common/db/utils.ts index cddb1880..195eddd4 100644 --- a/apps/auth-manager/src/common/db/utils.ts +++ b/apps/auth-manager/src/common/db/utils.ts @@ -1,4 +1,6 @@ -import { between, lt, gt, type SQL, type Column } from 'drizzle-orm'; +import { between, lt, gt, type SQL, type Column, asc, type AnyColumn, desc, type Subquery } from 'drizzle-orm'; +import type { PgTable } from 'drizzle-orm/pg-core'; +import type { SortOptions } from './sort'; export function createDatesComparison(column: Column, earlyDate?: Date, laterDate?: Date): SQL | undefined { if (earlyDate !== undefined && laterDate !== undefined) { @@ -12,3 +14,17 @@ export function createDatesComparison(column: Column, earlyDate?: Date, laterDat } return undefined; } + +export function sortOptionsToOrderBy(tableDefinition: T, sortOptions: SortOptions): SQL[] { + const result: SQL[] = []; + for (const key in sortOptions) { + const direction = sortOptions[key]; + + if (direction === 'asc') { + result.push(asc(tableDefinition[key] as AnyColumn)); + } else if (direction === 'desc') { + result.push(desc(tableDefinition[key] as AnyColumn)); + } + } + return result; +} diff --git a/apps/auth-manager/src/connection/DAL/connectionRepository.ts b/apps/auth-manager/src/connection/DAL/connectionRepository.ts index 68e45d8f..bee5f78c 100644 --- a/apps/auth-manager/src/connection/DAL/connectionRepository.ts +++ b/apps/auth-manager/src/connection/DAL/connectionRepository.ts @@ -1,54 +1,36 @@ -import type { Environments } from '@map-colonies/auth-core'; -import { Connection } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository, SelectQueryBuilder } from 'typeorm'; -import { DataSource } from 'typeorm'; +import { and, eq, max } from 'drizzle-orm'; +import { connectionTable, type Connection, type Drizzle, type DrizzleTx } from '@map-colonies/auth-core'; +import { inject, Lifecycle, scoped } from 'tsyringe'; +import { SERVICES } from '@common/constants'; -const maxVersionSubQuery = (qb: SelectQueryBuilder): string => { - const subQuery = qb - .subQuery() - .select('MAX(version)') - .from(Connection, 'connection') - .where('name = :name AND environment = :environment') - .getQuery(); +@scoped(Lifecycle.ContainerScoped) +export class ConnectionRepository { + public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} - return 'Connection.version = ' + subQuery; -}; + public async getMaxVersionWithLock(name: string, environment: Connection['environment'], tx: DrizzleTx): Promise { + const subQuery = tx + .select({ maxVersion: max(connectionTable.version) }) + .from(connectionTable) + .where(and(eq(connectionTable.name, name), eq(connectionTable.environment, environment))); -export type ConnectionRepository = Repository & { - getMaxVersionWithLock: (name: string, environment: Environments) => Promise; - getMaxVersion: (name: string, environment: Environments) => Promise; -}; + const result = await tx + .select({ version: connectionTable.version }) + .from(connectionTable) + .where(and(eq(connectionTable.name, name), eq(connectionTable.environment, environment), eq(connectionTable.version, subQuery))) + .for('update') + .limit(1); -export const connectionRepositoryFactory: FactoryFunction = (container) => { - const dataSource = container.resolve(DataSource); + return result[0]?.version ?? null; + } - return dataSource.getRepository(Connection).extend({ - async getMaxVersionWithLock(name: string, environment: Environments): Promise { - const result = await this.createQueryBuilder() - .select('version') - .where('name = :name AND environment = :environment') - .andWhere(maxVersionSubQuery) - .setLock('pessimistic_write') - .setParameters({ name, environment }) - .getRawOne<{ version: number }>(); + public async getMaxVersion(name: string, environment: Connection['environment'], tx?: DrizzleTx): Promise { + const db = tx ?? this.db; - if (result === undefined) { - return null; - } - return result.version; - }, - async getMaxVersion(name: string, environment: Environments): Promise { - const result = await this.createQueryBuilder() - .select('MAX(version)', 'version') - .where('name = :name AND environment = :environment') - .setParameters({ name, environment }) - .getRawOne<{ version: number }>(); + const result = await db + .select({ version: max(connectionTable.version) }) + .from(connectionTable) + .where(and(eq(connectionTable.name, name), eq(connectionTable.environment, environment))); - if (result === undefined) { - return null; - } - return result.version; - }, - }); -}; + return result[0]?.version ?? null; + } +} diff --git a/apps/auth-manager/src/connection/controllers/connectionController.ts b/apps/auth-manager/src/connection/controllers/connectionController.ts index e270775f..55083405 100644 --- a/apps/auth-manager/src/connection/controllers/connectionController.ts +++ b/apps/auth-manager/src/connection/controllers/connectionController.ts @@ -2,7 +2,7 @@ import { HttpError } from '@map-colonies/error-express-handler'; import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import { type Logger } from '@map-colonies/js-logger'; -import { IConnection } from '@map-colonies/auth-core'; +import { Connection } from '@map-colonies/auth-core'; import type { TypedRequestHandlers, components } from 'auth-openapi'; import { SERVICES } from '@common/constants'; import { ClientNotFoundError } from '@client/models/errors'; @@ -13,14 +13,14 @@ import { KeyNotFoundError } from '@key/models/errors'; import { ConnectionManager } from '../models/connectionManager'; import { ConnectionNotFoundError, ConnectionVersionMismatchError } from '../models/errors'; -function responseConnectionToOpenApi(connection: IConnection): components['schemas']['connection'] { +function responseConnectionToOpenApi(connection: Connection): components['schemas']['connection'] { return { ...connection, - createdAt: connection.createdAt?.toISOString(), + createdAt: connection.createdAt.toISOString(), }; } -const connectionSortMap = new Map([ +const connectionSortMap = new Map([ ['name', 'name'], ['environment', 'environment'], ['version', 'version'], diff --git a/apps/auth-manager/src/connection/models/connectionManager.ts b/apps/auth-manager/src/connection/models/connectionManager.ts index 77ac44f4..64ecda17 100644 --- a/apps/auth-manager/src/connection/models/connectionManager.ts +++ b/apps/auth-manager/src/connection/models/connectionManager.ts @@ -1,31 +1,63 @@ import { type Logger } from '@map-colonies/js-logger'; -import { Client, Connection, Environments, IConnection } from '@map-colonies/auth-core'; +import { type Connection, connectionTable, type DrizzleTx, Environments, type NewConnection, type Drizzle } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -import { SelectQueryBuilder } from 'typeorm'; import { JWK } from 'jose'; import { paths } from 'auth-openapi'; +import { ilike, SQL, inArray, eq, arrayContains, count, and, desc, asc, countDistinct, sql } from 'drizzle-orm'; import { ClientNotFoundError } from '@client/models/errors'; +import { sortOptionsToOrderBy } from '@src/common/db/utils'; import { SERVICES } from '@common/constants'; -import { type DomainRepository } from '@domain/DAL/domainRepository'; +import { DomainRepository } from '@domain/DAL/domainRepository'; import { DomainNotFoundError } from '@domain/models/errors'; -import { type KeyRepository } from '@key/DAL/keyRepository'; +import { KeyRepository } from '@key/DAL/keyRepository'; import { generateToken } from '@common/crypto'; -import { PaginationParams, paginationParamsToFindOptions } from '@src/common/db/pagination'; +import { PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import { KeyNotFoundError } from '@key/models/errors'; import { SortOptions } from '@src/common/db/sort'; import { asteriskStringComparatorLast } from '@src/utils/utils'; -import { type ConnectionRepository } from '../DAL/connectionRepository'; +import { ConnectionRepository } from '../DAL/connectionRepository'; import { ConnectionVersionMismatchError, ConnectionNotFoundError } from './errors'; type ConnectionSearchParams = NonNullable; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function getSearchFilters(params: ConnectionSearchParams) { + const filters: SQL[] = []; + if (params.name !== undefined) { + // qb.andWhere('connection.name ILIKE :name', { name: `%${params.name}%` }); + filters.push(ilike(connectionTable.name, `%${params.name}%`)); + } + if (params.environment) { + // qb.andWhere('connection.environment IN (:...environment)', { environment: params.environment }); + filters.push(inArray(connectionTable.environment, params.environment)); + } + if (params.isNoBrowser !== undefined) { + // qb.andWhere('connection.allowNoBrowserConnection = :isNoBrowser', { isNoBrowser: params.isNoBrowser }); + filters.push(eq(connectionTable.allowNoBrowserConnection, params.isNoBrowser)); + } + if (params.isNoOrigin !== undefined) { + // qb.andWhere('connection.allowNoOriginConnection = :isNoOrigin', { isNoOrigin: params.isNoOrigin }); + filters.push(eq(connectionTable.allowNoOriginConnection, params.isNoOrigin)); + } + if (params.domains) { + // qb.andWhere('connection.domains @> :domains', { domains: params.domains }); + filters.push(arrayContains(connectionTable.domains, params.domains)); + } + if (params.isEnabled !== undefined) { + // qb.andWhere('connection.enabled = :isEnabled', { isEnabled: params.isEnabled }); + filters.push(eq(connectionTable.enabled, params.isEnabled)); + } + return and(...filters); +} + @injectable() export class ConnectionManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.CONNECTION_REPOSITORY) private readonly connectionRepository: ConnectionRepository, - @inject(SERVICES.DOMAIN_REPOSITORY) private readonly domainRepository: DomainRepository, - @inject(SERVICES.KEY_REPOSITORY) private readonly keyRepository: KeyRepository + @inject(ConnectionRepository) private readonly connectionRepository: ConnectionRepository, + @inject(DomainRepository) private readonly domainRepository: DomainRepository, + @inject(KeyRepository) private readonly keyRepository: KeyRepository, + @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} /** @@ -42,78 +74,54 @@ export class ConnectionManager { searchParams: ConnectionSearchParams, paginationParams?: PaginationParams, sortParams?: SortOptions - ): Promise<[IConnection[], number]> { + ): Promise<[Connection[], number]> { this.logger.info({ msg: 'fetching connections', searchParams }); + const filters = getSearchFilters(searchParams); - const qb = this.connectionRepository.createQueryBuilder('connection'); + const countQuery = + searchParams.onlyLatest === true + ? this.drizzle.select({ count: countDistinct(sql`(${connectionTable.name},${connectionTable.environment})`) }) + : this.drizzle.select({ count: count() }); - // 1. Apply Base Filters - this.applySearchFilters(qb, searchParams); + const countResult = await countQuery.from(connectionTable).where(filters); + const total = countResult[0]?.count ?? 0; - // 2. Calculate Total Count - let total: number; + const selectQuery = + searchParams.onlyLatest === true ? this.drizzle.selectDistinctOn([connectionTable.name, connectionTable.environment]) : this.drizzle.select(); - if (searchParams.onlyLatest!) { - // STRATEGY: Distinct Count - // Standard .getCount() returns total rows. We need total *unique clients and environments*. - // We clone the query to avoid modifying the main QB instance used for fetching data. - const countResult = await qb - .clone() - .select('COUNT(DISTINCT (connection.name, connection.environment))', 'count') - .getRawOne<{ count: string }>(); + const subQuery = selectQuery + .from(connectionTable) + .where(filters) + .orderBy(connectionTable.name, connectionTable.environment, desc(connectionTable.version)) + .as('sq'); - total = parseInt(countResult?.count ?? '0', 10); - } else { - // STRATEGY: Standard Count - total = await qb.getCount(); - } + const { limit, offset } = paginationParamsToOffsetAndLimit(paginationParams); - // 3. Apply Scope & Sorting - if (searchParams.onlyLatest!) { - // STRATEGY: Postgres DISTINCT ON - // We group by name/env and keep the first row Postgres sees. - qb.distinctOn(['connection.name', 'connection.environment']); - - // REQUIREMENT: Postgres mandates that DISTINCT ON columns match the initial ORDER BY keys. - // We must apply the user's sort direction to these keys first. - const nameOrder = sortParams?.name?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; - const envOrder = sortParams?.environment?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; - - qb.orderBy('connection.name', nameOrder).addOrderBy('connection.environment', envOrder); - - // CRITICAL: The "Latest" Logic - // Within the unique (name, env) group, we force a sort by version DESC. - // Since DISTINCT ON picks the *first* row it encounters, this ensures the "Latest" version is picked. - qb.addOrderBy('connection.version', 'DESC'); - } else if (sortParams) { - Object.entries(sortParams).forEach(([key, order]) => { - qb.addOrderBy(`connection.${key}`, order.toUpperCase() as 'ASC' | 'DESC'); - }); - } + const connections = await this.drizzle + .select() + .from(subQuery) + .where(filters) + .orderBy(...sortOptionsToOrderBy(subQuery, sortParams ?? {})) + .limit(limit) + .offset(offset); - // 4. Pagination & Execution - if (paginationParams) { - const { skip, take } = paginationParamsToFindOptions(paginationParams); - qb.skip(skip).take(take); - } - - const connections = await qb.getMany(); return [connections, total]; } - public async getConnection(name: string, environment: Environments, version: number): Promise { + public async getConnection(name: string, environment: Environments, version: number): Promise { this.logger.info({ msg: 'fetching connection', connection: { name, version, environment } }); - const connection = await this.connectionRepository.findOne({ where: { name, version } }); + // const connection = await this.connectionRepository.findOne({ where: { name, version } }); + const connection = await this.drizzle.query.connection.findFirst({ where: { name, version, environment } }); - if (connection === null) { + if (connection === undefined) { this.logger.debug('connection was not found in the database'); throw new ConnectionNotFoundError('connection was not found in the database'); } return connection; } - public async getLatestConnection(name: string, environment: Environments): Promise { + public async getLatestConnection(name: string, environment: Environments): Promise { this.logger.info({ msg: 'fetching latest connection', connection: { name, environment } }); const version = await this.connectionRepository.getMaxVersion(name, environment); @@ -124,27 +132,29 @@ export class ConnectionManager { return this.getConnection(name, environment, version); } - public async upsertConnection(connection: IConnection, ignoreTokenErrors = false): Promise { + public async upsertConnection(connection: NewConnection, ignoreTokenErrors = false): Promise { this.logger.info({ msg: 'upserting connection', connection: { environment: connection.environment, version: connection.version } }); - return this.connectionRepository.manager.transaction(async (transactionManager) => { - const connectionRepo = transactionManager.withRepository(this.connectionRepository); - const domainRepo = transactionManager.withRepository(this.domainRepository); + // return this.connectionRepository.manager.transaction(async (transactionManager) => { + return this.drizzle.transaction(async (tx) => { + // const connectionRepo = transactionManager.withRepository(this.connectionRepository); + // const domainRepo = transactionManager.withRepository(this.domainRepository); - const client = await transactionManager.getRepository(Client).findOneBy({ name: connection.name }); + // const client = await transactionManager.getRepository(Client).findOneBy({ name: connection.name }); + const client = await tx.query.client.findFirst({ where: { name: connection.name } }); - if (client === null) { + if (client === undefined) { throw new ClientNotFoundError('no client exists with given name'); } - const notExistingDomains = await domainRepo.checkInputForNonExistingDomains(connection.domains); + const notExistingDomains = await this.domainRepository.checkInputForNonExistingDomains(connection.domains); if (notExistingDomains.length > 0) { throw new DomainNotFoundError(`the following domains do not exist: ${notExistingDomains.join(', ')}`); } - connection.token = await this.handleToken(connection, transactionManager.withRepository(this.keyRepository), !ignoreTokenErrors); + connection.token = await this.handleToken(connection, tx, !ignoreTokenErrors); - const maxVersion = await connectionRepo.getMaxVersionWithLock(connection.name, connection.environment); + const maxVersion = await this.connectionRepository.getMaxVersionWithLock(connection.name, connection.environment, tx); connection.origins = connection.origins.sort(asteriskStringComparatorLast()); if (maxVersion === null) { @@ -155,7 +165,8 @@ export class ConnectionManager { } this.logger.info({ msg: 'creating new connection', connection: { clientName: connection.name, environment: connection.environment } }); // insert - return connectionRepo.save(connection); + // return connectionRepo.save(connection); + return (await tx.insert(connectionTable).values(connection).returning())[0] as Connection; } if (maxVersion !== connection.version) { @@ -167,16 +178,21 @@ export class ConnectionManager { this.logger.info({ msg: 'updating existing connection', connection: { clientName: connection.name, environment: connection.environment } }); // update - return connectionRepo.save({ ...connection, version: maxVersion + 1 }); + return ( + await tx + .update(connectionTable) + .set({ ...connection, version: maxVersion + 1 }) + .returning() + )[0] as Connection; }); } - private async handleToken(connection: IConnection, transactionKeyRepo: KeyRepository, throwOnError?: boolean): Promise { + private async handleToken(connection: NewConnection, transaction: DrizzleTx, throwOnError?: boolean): Promise { if (connection.token !== '') { return connection.token; } - const key = (await transactionKeyRepo.getLatestKeys()).find((key) => key.environment === connection.environment); + const key = (await this.keyRepository.getLatestKeys(transaction)).find((key) => key.environment === connection.environment); if (key?.privateKey === undefined) { this.logger.warn({ @@ -199,28 +215,4 @@ export class ConnectionManager { return ''; } } - - /** - * Centralized filter logic to avoid duplication - */ - private applySearchFilters(qb: SelectQueryBuilder, params: ConnectionSearchParams): void { - if (params.name !== undefined) { - qb.andWhere('connection.name ILIKE :name', { name: `%${params.name}%` }); - } - if (params.environment) { - qb.andWhere('connection.environment IN (:...environment)', { environment: params.environment }); - } - if (params.isNoBrowser !== undefined) { - qb.andWhere('connection.allowNoBrowserConnection = :isNoBrowser', { isNoBrowser: params.isNoBrowser }); - } - if (params.isNoOrigin !== undefined) { - qb.andWhere('connection.allowNoOriginConnection = :isNoOrigin', { isNoOrigin: params.isNoOrigin }); - } - if (params.domains) { - qb.andWhere('connection.domains @> :domains', { domains: params.domains }); - } - if (params.isEnabled !== undefined) { - qb.andWhere('connection.enabled = :isEnabled', { isEnabled: params.isEnabled }); - } - } } diff --git a/apps/auth-manager/src/domain/DAL/domainRepository.ts b/apps/auth-manager/src/domain/DAL/domainRepository.ts index 3420db6f..44d3f0a3 100644 --- a/apps/auth-manager/src/domain/DAL/domainRepository.ts +++ b/apps/auth-manager/src/domain/DAL/domainRepository.ts @@ -1,36 +1,16 @@ -import { Domain } from '@map-colonies/auth-core'; -import { SERVICES } from '@src/common/constants'; -import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { inject, Lifecycle, scoped, type FactoryFunction } from 'tsyringe'; -import type { Logger, Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; +import { inArray } from 'drizzle-orm'; +import { domainTable, type Drizzle } from '@map-colonies/auth-core'; +import { inject, injectable } from 'tsyringe'; +import { SERVICES } from '@common/constants'; -// export type DomainRepository = Repository & { -// checkInputForNonExistingDomains: (domainNames: string[]) => Promise; -// }; +@injectable() +export class DomainRepository { + public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} -// export const domainRepositoryFactory: FactoryFunction = (container) => { -// const dataSource = container.resolve(DataSource); + public async checkInputForNonExistingDomains(domainNames: string[]): Promise { + const existing = await this.db.select({ name: domainTable.name }).from(domainTable).where(inArray(domainTable.name, domainNames)); -// return dataSource.getRepository(Domain).extend({ -// async checkInputForNonExistingDomains(domainNames: string[]): Promise { -// // unnest is a postgresql only function on array datatype -// // I wrote raw sql because typeorm doesn't think that using a function in FROM is a real thing and treats it like a table name -// const res = (await this.manager.query( -// ` -// SELECT i.name FROM unnest($1::text[]) i(name) LEFT JOIN auth_manager.domain d ON i.name = d.name WHERE d.name is NULL`, -// [domainNames] -// )) as unknown as { name: string }[]; - -// return res.map((domain) => domain.name); -// }, -// }); -// }; - -@scoped(Lifecycle.ContainerScoped) -export class ConfigRepository { - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.DRIZZLE) private readonly drizzle: NodePgDatabase - ) {} + const existingNames = new Set(existing.map((d) => d.name)); + return domainNames.filter((name) => !existingNames.has(name)); + } } diff --git a/apps/auth-manager/src/key/DAL/keyRepository.ts b/apps/auth-manager/src/key/DAL/keyRepository.ts index 97cf1d8b..206a6ffc 100644 --- a/apps/auth-manager/src/key/DAL/keyRepository.ts +++ b/apps/auth-manager/src/key/DAL/keyRepository.ts @@ -1,71 +1,58 @@ -import type { Environments } from '@map-colonies/auth-core'; -import { Key } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; - -export type KeyRepository = Repository & { - getMaxVersionWithLock: (env: Environments) => Promise; - getLatestKeys: () => Promise; - getMaxVersion: (env: Environments) => Promise; -}; - -export const keyRepositoryFactory: FactoryFunction = (container) => { - const dataSource = container.resolve(DataSource); - - return dataSource.getRepository(Key).extend({ - async getMaxVersionWithLock(env: Environments): Promise { - const result = await this.createQueryBuilder() - .select('version') - .where('environment = :environment') - .andWhere((qb) => { - const subQuery = qb.subQuery().select('MAX(version)').from(Key, 'key').where('environment = :environment').getQuery(); - - return 'Key.version = ' + subQuery; - }) - .setLock('pessimistic_write') - .setParameter('environment', env) - .getRawOne<{ version: number }>(); - - if (result === undefined) { - return null; - } - return result.version; - }, - async getMaxVersion(env: Environments): Promise { - const result = await this.createQueryBuilder() - .select('MAX(version)', 'version') - .where('environment = :environment') - .setParameter('environment', env) - .getRawOne<{ version: number }>(); - - if (result === undefined) { - return null; - } - return result.version; - }, - async getLatestKeys(): Promise { - // This is a way to do it using window functions. i think its more efficient but its less readable. - // I left it here because i worked a lot on it, and it got some nice stuff for future reference. - // Both this and the version below returns the same result. - // const query = this.createQueryBuilder() - // .from((qb) => { - // return qb.select('*, ROW_NUMBER() OVER(PARTITION BY environment ORDER BY version desc) as rn').from(Key, 'key'); - // }, 'Key') - // .where('rn = 1'); - - // query.expressionMap.aliases = [query.expressionMap.aliases[1]]; - // if (query.expressionMap.mainAlias !== undefined) { - // query.expressionMap.mainAlias.metadata = query.connection.getMetadata(Key); - // } - - const query = this.createQueryBuilder('keys').innerJoin( - (qb) => qb.select('environment, MAX(version) as version').from(Key, 'k').groupBy('environment'), - 'max_keys', - 'keys.version = max_keys.version AND keys.environment = max_keys.environment' +import { and, eq, max } from 'drizzle-orm'; +import { keyTable, type Key, type Drizzle, type DrizzleTx } from '@map-colonies/auth-core'; +import { inject, injectable, Lifecycle, scoped } from 'tsyringe'; +import { SERVICES } from '@common/constants'; + +@scoped(Lifecycle.ContainerScoped) +export class KeyRepository { + public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} + + public async getMaxVersionWithLock(env: Key['environment'], tx: DrizzleTx): Promise { + const subQuery = tx + .select({ maxVersion: max(keyTable.version) }) + .from(keyTable) + .where(eq(keyTable.environment, env)); + + const result = await tx + .select({ version: keyTable.version }) + .from(keyTable) + .where(and(eq(keyTable.environment, env), eq(keyTable.version, subQuery))) + .for('update') + .limit(1); + + return result[0]?.version ?? null; + } + + public async getMaxVersion(env: Key['environment'], tx?: DrizzleTx): Promise { + const db = tx ?? this.db; + + const result = await db + .select({ version: max(keyTable.version) }) + .from(keyTable) + .where(eq(keyTable.environment, env)); + + return result[0]?.version ?? null; + } + + public async getLatestKeys(tx?: DrizzleTx): Promise { + const db = tx ?? this.db; + const maxVersionsSubQuery = db + .select({ environment: keyTable.environment, version: max(keyTable.version).as('version') }) + .from(keyTable) + .groupBy(keyTable.environment) + .as('max_keys'); + + return db + .select({ + environment: keyTable.environment, + version: keyTable.version, + privateKey: keyTable.privateKey, + publicKey: keyTable.publicKey, + }) + .from(keyTable) + .innerJoin( + maxVersionsSubQuery, + and(eq(keyTable.version, maxVersionsSubQuery.version), eq(keyTable.environment, maxVersionsSubQuery.environment)) ); - - return query.getMany(); - }, - }); -}; + } +} diff --git a/apps/auth-manager/src/key/models/keyManager.ts b/apps/auth-manager/src/key/models/keyManager.ts index 2dfa5dca..6a133ebb 100644 --- a/apps/auth-manager/src/key/models/keyManager.ts +++ b/apps/auth-manager/src/key/models/keyManager.ts @@ -1,45 +1,45 @@ import { type Logger } from '@map-colonies/js-logger'; -import { Environments, IKey } from '@map-colonies/auth-core'; +import { type Drizzle, Environments, Key, keyTable, NewKey } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -import type { SetRequired } from 'type-fest'; import { SERVICES } from '@common/constants'; -import { type KeyRepository } from '../DAL/keyRepository'; +import { KeyRepository } from '../DAL/keyRepository'; import { KeyVersionMismatchError, KeyNotFoundError } from './errors'; -type ResponseKey = SetRequired; - @injectable() export class KeyManager { public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.KEY_REPOSITORY) private readonly keyRepository: KeyRepository + @inject(KeyRepository) private readonly keyRepository: KeyRepository, + @inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle ) {} - public async getLatestKeys(): Promise { + public async getLatestKeys(): Promise { this.logger.info({ msg: 'fetching latest keys' }); return this.keyRepository.getLatestKeys(); } - public async getEnvKeys(environment: Environments): Promise { + public async getEnvKeys(environment: Environments): Promise { this.logger.info({ msg: 'fetching all specific environment keys', key: { environment } }); - return this.keyRepository.find({ where: { environment } }); + // return this.keyRepository.find({ where: { environment } }); + return this.drizzle.query.key.findMany({ where: { environment } }); } - public async getKey(environment: Environments, version: number): Promise { + public async getKey(environment: Environments, version: number): Promise { this.logger.info({ msg: 'fetching key', key: { environment, version } }); - const key = await this.keyRepository.findOne({ where: { environment, version } }); + // const key = await this.keyRepository.findOne({ where: { environment, version } }); + const key = await this.drizzle.query.key.findFirst({ where: { environment, version } }); - if (key === null) { + if (key === undefined) { this.logger.debug('key was not found in the database'); throw new KeyNotFoundError('key was not found in the database'); } return key; } - public async getLatestKey(environment: Environments): Promise { + public async getLatestKey(environment: Environments): Promise { this.logger.info({ msg: 'fetching latest key', key: { environment } }); const version = await this.keyRepository.getMaxVersion(environment); if (version === null) { @@ -49,12 +49,36 @@ export class KeyManager { return this.getKey(environment, version); } - public async upsertKey(key: IKey): Promise { + public async upsertKey(key: NewKey): Promise { this.logger.info({ msg: 'upserting key', key: { environment: key.environment, version: key.version } }); - return this.keyRepository.manager.transaction(async (transactionManager) => { - const transactionRepo = transactionManager.withRepository(this.keyRepository); + // return this.keyRepository.manager.transaction(async (transactionManager) => { + // const transactionRepo = transactionManager.withRepository(this.keyRepository); + + // const maxVersion = await transactionRepo.getMaxVersionWithLock(key.environment); + + // if (maxVersion === null) { + // if (key.version !== 1) { + // const msg = 'given key version is not 1, when no key already exists'; + // this.logger.debug({ msg, clientKeyVersion: key.version }); + // throw new KeyVersionMismatchError(msg); + // } + + // // insert + // return transactionRepo.save(key); + // } + + // if (maxVersion !== key.version) { + // const msg = 'version mismatch between database key and given key'; + // this.logger.debug({ msg, clientKeyVersion: key.version, dbKeyVersion: maxVersion }); + + // throw new KeyVersionMismatchError(msg); + // } - const maxVersion = await transactionRepo.getMaxVersionWithLock(key.environment); + // // update + // return transactionRepo.save({ ...key, version: maxVersion + 1 }); + // }); + return this.drizzle.transaction(async (tx) => { + const maxVersion = await this.keyRepository.getMaxVersionWithLock(key.environment, tx); if (maxVersion === null) { if (key.version !== 1) { @@ -64,7 +88,8 @@ export class KeyManager { } // insert - return transactionRepo.save(key); + const res = await tx.insert(keyTable).values(key).returning(); + return res[0] as Key; } if (maxVersion !== key.version) { @@ -75,7 +100,11 @@ export class KeyManager { } // update - return transactionRepo.save({ ...key, version: maxVersion + 1 }); + const res = await tx + .insert(keyTable) + .values({ ...key, version: maxVersion + 1 }) + .returning(); + return res[0] as Key; }); } } diff --git a/apps/auth-manager/tests/configurations/vitest.globalSetup.mts b/apps/auth-manager/tests/configurations/vitest.globalSetup.mts index 8ca46e44..e5b200b2 100644 --- a/apps/auth-manager/tests/configurations/vitest.globalSetup.mts +++ b/apps/auth-manager/tests/configurations/vitest.globalSetup.mts @@ -19,5 +19,5 @@ export async function setup(): Promise { const connection = await initConnection({ ...dataSourceOptions, port }); await mergeTestConfig(path.join(__dirname, '../../config'), { 'db.port': port }); - await resetAndMigrate(connection, dataSourceOptions.schema); + await resetAndMigrate(connection); } diff --git a/apps/auth-manager/tests/integration/asset/asset.spec.mts b/apps/auth-manager/tests/integration/asset/asset.spec.mts index bcb8ee8d..fbd5489c 100644 --- a/apps/auth-manager/tests/integration/asset/asset.spec.mts +++ b/apps/auth-manager/tests/integration/asset/asset.spec.mts @@ -1,58 +1,44 @@ /// -import { describe, expect, it, vi, beforeAll, afterAll, afterEach } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import { trace } from '@opentelemetry/api'; +import { describe, expect, it, vi, beforeAll, afterEach } from 'vitest'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; import 'jest-openapi'; -import { DataSource } from 'typeorm'; -import type { IAsset } from '@map-colonies/auth-core'; -import { assetTable, AssetType, Environment } from '@map-colonies/auth-core'; +import { type Asset, assetTable, AssetType, type Drizzle, Environment, type NewAsset } from '@map-colonies/auth-core'; import { faker } from '@faker-js/faker'; import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; import { getFakeAsset } from 'test-utils'; import type { paths, operations } from 'auth-openapi'; -import type { AssetRepository } from '@src/asset/DAL/assetRepository.js'; -import { getApp } from '@src/app.js'; -import { SERVICES } from '@common/constants.js'; -import { initConfig } from '@common/config.js'; -import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; +import { AssetRepository } from '@src/asset/DAL/assetRepository.js'; +import { initEnvironment } from '../setup.js'; describe('client', function () { let requestSender: RequestSender; let depContainer: DependencyContainer; + let drizzle: Drizzle; beforeAll(async function () { - await initConfig(true); - const [app, container] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender(OPENAPI_PATH, app); - depContainer = container; + const env = await initEnvironment(); + depContainer = env.container; + requestSender = env.requestSender; + drizzle = env.drizzle; }); - afterAll(async function () { - await depContainer.resolve(DataSource).destroy(); - }); + // afterAll(async function () { + // await depContainer.resolve(Pool).end(); + // }); describe('Happy Path', function () { describe('GET /assets', function () { it('should return 200 status code and all the assets', async function () { const asset = getFakeAsset(); - asset.environment = [Environment.PRODUCTION]; - const assets: IAsset[] = [ + asset.environment = [Environment.PROD]; + const assets: NewAsset[] = [ asset, { ...asset, version: 2 }, { ...asset, name: faker.string.sample(9), environment: [Environment.NP, Environment.STAGE] }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const res = await requestSender.getAssets(); @@ -63,39 +49,37 @@ describe('client', function () { it('should return 200 status code and all the assets with specific env', async function () { const asset = getFakeAsset(); - asset.environment = [Environment.PRODUCTION]; - const assets: IAsset[] = [ + asset.environment = [Environment.PROD]; + const assets: NewAsset[] = [ asset, { ...asset, version: 2 }, { ...asset, name: faker.string.sample(9), environment: [Environment.NP, Environment.STAGE] }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const res = await requestSender.getAssets({ queryParams: { environment: ['prod'] } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toSatisfyAll((a: IAsset) => a.environment.includes(Environment.PRODUCTION)); + expect(res.body).toSatisfyAll((a: Asset) => a.environment.includes(Environment.PROD)); }); it('should return 200 status code and all the assets with specific type', async function () { const asset = getFakeAsset(); asset.type = AssetType.DATA; - const assets: IAsset[] = [ + const assets: NewAsset[] = [ { ...asset, name: faker.string.sample(9) }, { ...asset, type: AssetType.POLICY }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const res = await requestSender.getAssets({ queryParams: { type: AssetType.DATA } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toSatisfyAll((a: IAsset) => a.type === AssetType.DATA); + expect(res.body).toSatisfyAll((a: Asset) => a.type === AssetType.DATA); }); }); @@ -103,67 +87,64 @@ describe('client', function () { it('should return 201 status code and the created asset', async function () { const asset = getFakeAsset(); - const res = await requestSender.upsertAsset({ requestBody: asset }); + const res = await requestSender.upsertAsset({ requestBody: { ...asset, value: asset.value.toString('base64') } }); delete asset.createdAt; expect(res).toHaveProperty('status', httpStatusCodes.CREATED); expect(res).toSatisfyApiSpec(); - expect(res.body).toMatchObject(asset); + expect(res.body).toMatchObject({ ...asset, value: asset.value.toString('base64') }); }); it('should return 200 status code and the updated asset', async function () { const asset = getFakeAsset(); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(asset); + await drizzle.insert(assetTable).values(asset).execute(); delete asset.createdAt; - const res = await requestSender.upsertAsset({ requestBody: asset }); + const res = await requestSender.upsertAsset({ requestBody: { ...asset, value: asset.value.toString('base64') } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toMatchObject({ ...asset, version: 2 }); + expect(res.body).toMatchObject({ ...asset, version: 2, value: asset.value.toString('base64') }); }); }); describe('GET /asset/:name', function () { it('should return 200 status code all the assets with the specific name', async function () { const asset = getFakeAsset(); - const assets: IAsset[] = [asset, { ...asset, version: 2 }]; + const assets: NewAsset[] = [asset, { ...asset, version: 2 }]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const res = await requestSender.getAsset({ pathParams: { assetName: asset.name } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toSatisfyAll((a: IAsset) => a.name === asset.name); + expect(res.body).toSatisfyAll((a: NewAsset) => a.name === asset.name); }); }); describe('GET /asset/:name/:version', function () { it('should return 200 status code and the requested asset', async function () { const asset = getFakeAsset(); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(asset); + await drizzle.insert(assetTable).values(asset).execute(); const res = await requestSender.getVersionedAsset({ pathParams: { assetName: asset.name, version: asset.version } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toStrictEqual({ ...asset, createdAt: asset.createdAt?.toISOString() }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect(res.body).toStrictEqual({ ...asset, value: asset.value.toString('base64'), createdAt: expect.any(String) }); }); }); describe('GET /asset/:name/latest', function () { it('should return 200 status code and the latest asset when multiple versions exist', async function () { const baseAsset = getFakeAsset(); - const assets: IAsset[] = [baseAsset, { ...baseAsset, version: 2 }, { ...baseAsset, version: 3 }]; + const assets: NewAsset[] = [baseAsset, { ...baseAsset, version: 2 }, { ...baseAsset, version: 3 }]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const expectedAsset = assets.find((a) => a.version === 3); @@ -171,19 +152,20 @@ describe('client', function () { expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toStrictEqual({ ...expectedAsset, createdAt: expectedAsset!.createdAt?.toISOString() }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect(res.body).toStrictEqual({ ...expectedAsset, value: expectedAsset!.value.toString('base64'), createdAt: expect.any(String) }); }); it('should return 200 status code and the only asset when there is only one version', async function () { const asset = getFakeAsset(); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save(asset); + await drizzle.insert(assetTable).values(asset).execute(); const res = await requestSender.getLatestAsset({ pathParams: { assetName: asset.name } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toStrictEqual({ ...asset, createdAt: asset.createdAt?.toISOString() }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect(res.body).toStrictEqual({ ...asset, createdAt: expect.any(String), value: asset.value.toString('base64') }); }); }); }); @@ -192,7 +174,12 @@ describe('client', function () { describe('POST /asset', function () { it('should return 400 if the request body is incorrect', async function () { const { version, ...asset } = getFakeAsset(); - const res = await requestSender.upsertAsset({ requestBody: asset as IAsset }); + const res = await requestSender.upsertAsset({ + requestBody: { + ...asset, + value: asset.value.toString('base64'), + } as unknown as paths['/asset']['post']['requestBody']['content']['application/json'], + }); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); expect(res).toSatisfyApiSpec(); @@ -200,17 +187,21 @@ describe('client', function () { it("should return 409 if the request version doesn't match the DB version", async function () { const asset = getFakeAsset(); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(assetTable).save({ ...asset }); + await drizzle + .insert(assetTable) + .values({ ...asset }) + .execute(); - const res = await requestSender.upsertAsset({ requestBody: { ...asset, version: 2 } }); + const res = await requestSender.upsertAsset({ requestBody: { ...asset, value: asset.value.toString('base64'), version: 2 } }); expect(res).toHaveProperty('status', httpStatusCodes.CONFLICT); expect(res).toSatisfyApiSpec(); }); it("should return 409 if the no asset exists and request version isn't 1", async function () { - const res = await requestSender.upsertAsset({ requestBody: { ...getFakeAsset(), version: 2 } }); + const res = await requestSender.upsertAsset({ + requestBody: { ...getFakeAsset(), value: getFakeAsset().value.toString('base64'), version: 2 }, + }); expect(res).toHaveProperty('status', httpStatusCodes.CONFLICT); expect(res).toSatisfyApiSpec(); @@ -266,8 +257,9 @@ describe('client', function () { describe('GET /asset', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); - vi.spyOn(repo, 'findBy').mockRejectedValue(new Error()); + // const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); + // vi.spyOn(repo, 'findBy').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.asset, 'findMany').mockRejectedValue(new Error()); const res = await requestSender.getAssets(); @@ -278,11 +270,11 @@ describe('client', function () { describe('POST /asset', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); + const repo = depContainer.resolve(AssetRepository); vi.spyOn(repo, 'getMaxVersionWithLock').mockRejectedValue(new Error()); const asset = getFakeAsset(); - const res = await requestSender.upsertAsset({ requestBody: asset }); + const res = await requestSender.upsertAsset({ requestBody: { ...asset, value: asset.value.toString('base64') } }); expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); expect(res).toSatisfyApiSpec(); @@ -290,8 +282,9 @@ describe('client', function () { describe('GET /asset/:name', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); - vi.spyOn(repo, 'findBy').mockRejectedValue(new Error()); + // const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); + // vi.spyOn(repo, 'findBy').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.asset, 'findMany').mockRejectedValue(new Error()); const res = await requestSender.getAsset({ pathParams: { assetName: 'avi' } }); @@ -302,8 +295,9 @@ describe('client', function () { describe('GET /asset/:name/:version', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); - vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + // const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); + // vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.asset, 'findFirst').mockRejectedValue(new Error()); const res = await requestSender.getVersionedAsset({ pathParams: { assetName: 'avi', version: 1 } }); @@ -314,7 +308,7 @@ describe('client', function () { describe('GET /asset/:name/latest', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); + const repo = depContainer.resolve(AssetRepository); vi.spyOn(repo, 'getMaxVersion').mockRejectedValue(new Error()); const res = await requestSender.getLatestAsset({ pathParams: { assetName: 'avi' } }); diff --git a/apps/auth-manager/tests/integration/bundle/bundle.spec.mts b/apps/auth-manager/tests/integration/bundle/bundle.spec.mts index a915d8a9..d2520bdf 100644 --- a/apps/auth-manager/tests/integration/bundle/bundle.spec.mts +++ b/apps/auth-manager/tests/integration/bundle/bundle.spec.mts @@ -1,47 +1,37 @@ /// -import { afterEach, describe, expect, it, vi, beforeAll, afterAll } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import { trace } from '@opentelemetry/api'; +import { afterEach, describe, expect, it, vi, beforeAll } from 'vitest'; import { faker } from '@faker-js/faker'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; -import type { Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; -import type { Environments, IBundle } from '@map-colonies/auth-core'; -import { Bundle, Environment } from '@map-colonies/auth-core'; +import type { Drizzle, Environments, Bundle } from '@map-colonies/auth-core'; +import { bundleTable, Environment } from '@map-colonies/auth-core'; import 'jest-openapi'; -import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import type { ExpectResponseStatus, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { expectResponseStatusFactory } from '@map-colonies/openapi-helpers/requestSender'; import { getFakeBundle } from 'test-utils'; import type { paths, operations } from 'auth-openapi'; -import { getApp } from '@src/app.js'; -import { SERVICES } from '@common/constants.js'; -import { initConfig } from '@common/config.js'; -import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; +import { initEnvironment } from '../setup.js'; + +const expectResponseStatus: ExpectResponseStatus = expectResponseStatusFactory(expect); +type ApiBundle = operations['getBundle']['responses']['200']['content']['application/json']; describe('bundle', function () { let requestSender: RequestSender; - let depContainer: DependencyContainer; - const bundles = [getFakeBundle(), { ...getFakeBundle(), environment: Environment.PRODUCTION }, getFakeBundle()]; + let drizzle: Drizzle; + let bundles: [ApiBundle, ApiBundle, ApiBundle]; beforeAll(async function () { - await initConfig(true); - const [app, container] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender(OPENAPI_PATH, app); - depContainer = container; - - await container.resolve(DataSource).getRepository(Bundle).save(bundles); - bundles.forEach((b) => delete b.createdAt); - }); - - afterAll(async function () { - await depContainer.resolve(DataSource).destroy(); + const env = await initEnvironment(); + requestSender = env.requestSender; + drizzle = env.drizzle; + + bundles = ( + await drizzle + .insert(bundleTable) + .values([getFakeBundle(), { ...getFakeBundle(), environment: Environment.PROD }, getFakeBundle()]) + .returning() + ).map((b) => ({ ...b, createdAt: b.createdAt.toISOString() })) as [ApiBundle, ApiBundle, ApiBundle]; + // bundles.forEach((b) => delete b.createdAt); }); describe('Happy Path', function () { @@ -63,8 +53,11 @@ describe('bundle', function () { }); it('should support filtering bundles by createdBefore and createdAfter', async function () { - const bundle = await requestSender.getBundle({ pathParams: { id: bundles[0]?.id as number } }); - const bundleBody = bundle.body as IBundle; + const bundle = await requestSender.getBundle({ pathParams: { id: bundles[0].id as number } }); + + expectResponseStatus(bundle, httpStatusCodes.OK); + const bundleBody = bundle.body; + const createdBefore = faker.date.future({ refDate: bundleBody.createdAt }); const createdAfter = faker.date.past({ refDate: bundleBody.createdAt }); @@ -79,12 +72,11 @@ describe('bundle', function () { describe('GET /bundle/:id', function () { it('should return 201 status code and the created bundle', async function () { - const firstBundle = bundles[0] as IBundle; - const res = await requestSender.getBundle({ pathParams: { id: firstBundle.id as number } }); + const res = await requestSender.getBundle({ pathParams: { id: bundles[0].id as number } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toMatchObject(firstBundle); + expect(res.body).toMatchObject(bundles[0]); }); }); }); @@ -124,9 +116,7 @@ describe('bundle', function () { describe('GET /bundle', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve>(SERVICES.BUNDLE_REPOSITORY); - const spy = vi.spyOn(repo, 'findBy'); - spy.mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.bundle, 'findMany').mockRejectedValue(new Error()); const res = await requestSender.getBundles(); expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); @@ -136,9 +126,7 @@ describe('bundle', function () { describe('GET /bundle/:id', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve>(SERVICES.BUNDLE_REPOSITORY); - - vi.spyOn(repo, 'findOneBy').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.bundle, 'findFirst').mockRejectedValue(new Error()); const res = await requestSender.getBundle({ pathParams: { id: 1 } }); diff --git a/apps/auth-manager/tests/integration/client/client.spec.mts b/apps/auth-manager/tests/integration/client/client.spec.mts index 41c0771e..6dc0cc98 100644 --- a/apps/auth-manager/tests/integration/client/client.spec.mts +++ b/apps/auth-manager/tests/integration/client/client.spec.mts @@ -1,63 +1,47 @@ /// -import { beforeAll, describe, expect, it, afterAll, afterEach, vi } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import { trace } from '@opentelemetry/api'; +import { beforeAll, describe, expect, it, afterEach, vi } from 'vitest'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; import { faker } from '@faker-js/faker'; import 'jest-openapi'; -import { DataSource } from 'typeorm'; -import type { IClient } from '@map-colonies/auth-core'; -import { Client } from '@map-colonies/auth-core'; -import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import type { Drizzle } from '@map-colonies/auth-core'; +import { clientTable } from '@map-colonies/auth-core'; +import type { ExpectResponseStatus, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { expectResponseStatusFactory } from '@map-colonies/openapi-helpers/requestSender'; import { getFakeClient } from 'test-utils'; import type { paths, operations } from 'auth-openapi'; -import { getApp } from '@src/app.js'; -import { SERVICES } from '@common/constants.js'; -import { initConfig } from '@common/config.js'; -import type { ClientRepository } from '@src/client/DAL/clientRepository.js'; -import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; +import { initEnvironment } from '../setup.js'; + +type ApiClient = paths['/client']['post']['requestBody']['content']['application/json']; + +const expectResponseStatus: ExpectResponseStatus = expectResponseStatusFactory(expect); describe('client', function () { let requestSender: RequestSender; - let depContainer: DependencyContainer; + let drizzle: Drizzle; beforeAll(async function () { - await initConfig(true); - const [app, container] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender(OPENAPI_PATH, app); - depContainer = container; - }); - - afterAll(async function () { - await depContainer.resolve(DataSource).destroy(); + const env = await initEnvironment(); + requestSender = env.requestSender; + drizzle = env.drizzle; }); describe('Happy Path', function () { describe('GET /client', function () { afterEach(async function () { - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).clear(); + await drizzle.delete(clientTable); }); it('should return 200 status code and a list of clients', async function () { const clients = [getFakeClient(false), getFakeClient(false)]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + // const connection = depContainer.resolve(DataSource); + await drizzle.insert(clientTable).values(clients).execute(); const res = await requestSender.getClients(); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toIncludeAllPartialMembers(clients); }); @@ -69,8 +53,7 @@ describe('client', function () { { name: 'avi', searchParam: 'AV', matchType: 'case-insensitive' }, ])('should find the user $name with search string $searchParam with match type $matchType', async function ({ name, searchParam }) { const client = { ...getFakeClient(false), name }; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(client); + await drizzle.insert(clientTable).values(client).execute(); const res = await requestSender.getClients({ queryParams: { @@ -80,14 +63,14 @@ describe('client', function () { expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); + console.log(res.body); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect((res.body as Exclude).items).toSatisfyAny((item) => item.name === name); }); it('should return empty array when no clients match the name search param', async function () { const client = { ...getFakeClient(false), name: 'bla' }; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(client); + await drizzle.insert(clientTable).values(client); const res = await requestSender.getClients({ queryParams: { @@ -95,9 +78,8 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(0); }); @@ -107,8 +89,8 @@ describe('client', function () { { ...getFakeClient(false), createdAt: new Date('2023-01-01') }, { ...getFakeClient(false), createdAt: new Date('2023-02-01') }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); + const res = await requestSender.getClients({ queryParams: { createdAfter: new Date('2022-12-31').toISOString(), @@ -116,14 +98,12 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(2); }); it('should support basic pagination with page and pageSize', async function () { - // Generated by Copilot const TOTAL_CLIENTS = 5; const PAGE_SIZE = 2; const TARGET_PAGE = 1; @@ -133,8 +113,7 @@ describe('client', function () { name: `pagination-client-${String(index).padStart(2, '0')}`, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { @@ -144,11 +123,9 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(PAGE_SIZE); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); }); @@ -163,8 +140,7 @@ describe('client', function () { name: `page-test-client-${String(index).padStart(2, '0')}`, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { @@ -174,11 +150,9 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(PAGE_SIZE); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); }); @@ -194,8 +168,7 @@ describe('client', function () { name: `last-page-client-${String(index).padStart(2, '0')}`, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { @@ -205,16 +178,13 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(EXPECTED_ITEMS_ON_LAST_PAGE); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); }); it('should support sorting by name in ascending order', async function () { - // Generated by Copilot const clientNames = ['zebra-client', 'alpha-client', 'beta-client']; const sortedNames = ['alpha-client', 'beta-client', 'zebra-client']; @@ -223,8 +193,7 @@ describe('client', function () { name, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { @@ -232,21 +201,18 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly + expect(res.body.items).toHaveLength(clientNames.length); // Check if items are sorted correctly for (let i = 0; i < sortedNames.length; i++) { - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(res.body.items[i].name).toBe(sortedNames[i]); + expect(res.body.items[i]?.name).toBe(sortedNames[i]); } }); it('should support sorting by name in descending order', async function () { - // Generated by Copilot const clientNames = ['alpha-client', 'zebra-client', 'beta-client']; const sortedNamesDesc = ['zebra-client', 'beta-client', 'alpha-client']; @@ -255,27 +221,23 @@ describe('client', function () { name, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); - + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { sort: ['name:desc'], }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - const items = res.body.items as IClient[]; + const items = res.body.items; expect(items).toHaveLength(clientNames.length); // Check if items are sorted correctly in descending order for (let i = 0; i < sortedNamesDesc.length; i++) { - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - expect(items[i].name).toBe(sortedNamesDesc[i]); + expect(items[i]?.name).toBe(sortedNamesDesc[i]); } }); @@ -289,8 +251,7 @@ describe('client', function () { createdAt: date, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { @@ -298,22 +259,20 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toHaveLength(dates.length); // Check if items are sorted by createdAt in ascending order - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - const sortedItems = res.body.items as Client[]; + const sortedItems = res.body.items; for (let i = 0; i < sortedItems.length - 1; i++) { // Generated by Copilot const currentItem = sortedItems[i]; const nextItem = sortedItems[i + 1]; // Ensure createdAt exists before creating Date objects - if (!currentItem?.createdAt || !nextItem?.createdAt) { - throw new Error('createdAt is required for date comparison'); + if (currentItem?.createdAt === undefined || nextItem?.createdAt === undefined) { + expect.fail('createdAt is required for date comparison'); } const currentDate = new Date(currentItem.createdAt); @@ -324,7 +283,6 @@ describe('client', function () { }); it('should combine pagination and sorting', async function () { - // Generated by Copilot const TOTAL_CLIENTS = 6; const PAGE_SIZE = 2; const TARGET_PAGE = 2; @@ -334,9 +292,7 @@ describe('client', function () { name: `combo-client-${String(index).padStart(2, '0')}`, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); - + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { page: TARGET_PAGE, @@ -346,14 +302,12 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - const returnedItems = res.body.items as IClient[]; + const returnedItems = res.body.items; expect(returnedItems).toBeArrayOfSize(PAGE_SIZE); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); // Verify the specific items on page 2 with sorting const isFirstItemCorrect = returnedItems[0]?.name === 'combo-client-02'; @@ -374,8 +328,9 @@ describe('client', function () { name: `empty-test-client-${index}`, })); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + // const connection = depContainer.resolve(DataSource); + // await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); + await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ queryParams: { @@ -385,16 +340,14 @@ describe('client', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(0); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); }); it('should return 201 status code and the created client', async function () { - const client = getFakeClient(false); + const client = getFakeClient(false) as unknown as ApiClient; const res = await requestSender.createClient({ requestBody: client }); @@ -408,8 +361,9 @@ describe('client', function () { it('should return 200 status code and the client', async function () { const client = getFakeClient(false); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert({ ...client }); + // const connection = depContainer.resolve(DataSource); + // await connection.getRepository(Client).insert({ ...client }); + await drizzle.insert(clientTable).values(client); const res = await requestSender.getClient({ pathParams: { clientName: client.name } }); @@ -423,12 +377,17 @@ describe('client', function () { it('should return 200 status code and the updated client', async function () { const client = getFakeClient(false); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Client).insert({ ...client }); + // const connection = depContainer.resolve(DataSource); + // await connection.getRepository(Client).insert({ ...client }); + await drizzle.insert(clientTable).values(client); const res = await requestSender.updateClient({ pathParams: { clientName: client.name }, - requestBody: { ...client, description: 'xd', tags: ['a', 'b'] }, + requestBody: { + ...client, + description: 'xd', + tags: ['a', 'b'], + } as unknown as ApiClient, }); expect(res).toHaveProperty('status', httpStatusCodes.OK); @@ -441,7 +400,7 @@ describe('client', function () { describe('Bad Path', function () { describe('POST /client', function () { it('should return 400 status code if the name is too short', async function () { - const client = getFakeClient(false); + const client = getFakeClient(false) as unknown as ApiClient; client.name = 'a'; const res = await requestSender.createClient({ requestBody: client }); @@ -452,7 +411,7 @@ describe('client', function () { }); it('should return 400 status code if the name is too long', async function () { - const client = getFakeClient(false); + const client = getFakeClient(false) as unknown as ApiClient; client.name = faker.string.alpha(33); const res = await requestSender.createClient({ requestBody: client }); @@ -463,7 +422,7 @@ describe('client', function () { }); it('should return 409 status code if client with the same name already exists', async function () { - const client = getFakeClient(false); + const client = getFakeClient(false) as unknown as ApiClient; const res1 = await requestSender.createClient({ requestBody: client }); @@ -489,7 +448,7 @@ describe('client', function () { describe('PATCH /client/:clientName', function () { it('should return 404 status code if the client was not found', async function () { - const client = getFakeClient(false); + const client = getFakeClient(false) as unknown as ApiClient; const res = await requestSender.updateClient({ pathParams: { clientName: 'lol' }, @@ -510,8 +469,9 @@ describe('client', function () { describe('GET /client', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - vi.spyOn(repo, 'findAndCount').mockRejectedValue(new Error()); + // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); + // vi.spyOn(repo, 'findAndCount').mockRejectedValue(new Error()); + vi.spyOn(drizzle, 'select').mockRejectedValue(new Error()); const res = await requestSender.getClients(); @@ -522,40 +482,43 @@ describe('client', function () { describe('POST /client', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - vi.spyOn(repo, 'insert').mockRejectedValue(new Error()); + // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); + // vi.spyOn(repo, 'insert').mockRejectedValue(new Error()); + vi.spyOn(drizzle, 'insert').mockRejectedValue(new Error()); - const res = await requestSender.createClient({ requestBody: getFakeClient(false) }); + const res = await requestSender.createClient({ requestBody: getFakeClient(false) as unknown as ApiClient }); expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); expect(res).toSatisfyApiSpec(); }); + }); - describe('GET /client/:clientName', function () { - it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + describe('GET /client/:clientName', function () { + it('should return 500 status code if db throws an error', async function () { + // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); + // vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.client, 'findFirst').mockRejectedValue(new Error()); - const res = await requestSender.getClient({ pathParams: { clientName: 'avi' } }); + const res = await requestSender.getClient({ pathParams: { clientName: 'avi' } }); - expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); - expect(res).toSatisfyApiSpec(); - }); + expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(res).toSatisfyApiSpec(); }); + }); - describe('PATCH /client/:clientName', function () { - it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - vi.spyOn(repo, 'updateAndReturn').mockRejectedValue(new Error()); - - const res = await requestSender.updateClient({ - pathParams: { clientName: 'avi' }, - requestBody: getFakeClient(false), - }); + describe('PATCH /client/:clientName', function () { + it('should return 500 status code if db throws an error', async function () { + // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); + // vi.spyOn(repo, 'updateAndReturn').mockRejectedValue(new Error()); + vi.spyOn(drizzle, 'update').mockRejectedValue(new Error()); - expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); - expect(res).toSatisfyApiSpec(); + const res = await requestSender.updateClient({ + pathParams: { clientName: 'avi' }, + requestBody: getFakeClient(false) as unknown as ApiClient, }); + + expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(res).toSatisfyApiSpec(); }); }); }); diff --git a/apps/auth-manager/tests/integration/connection/connection.spec.mts b/apps/auth-manager/tests/integration/connection/connection.spec.mts index ad4de15b..078e5c9c 100644 --- a/apps/auth-manager/tests/integration/connection/connection.spec.mts +++ b/apps/auth-manager/tests/integration/connection/connection.spec.mts @@ -1,67 +1,59 @@ /// -import { beforeEach, describe, expect, it, vi, beforeAll, afterEach, afterAll } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import { trace } from '@opentelemetry/api'; +import { beforeEach, describe, expect, it, vi, beforeAll, afterEach } from 'vitest'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; import 'jest-openapi'; -import { DataSource } from 'typeorm'; -import type { Environments, IConnection } from '@map-colonies/auth-core'; -import { Client, Connection, Domain, Environment, Key } from '@map-colonies/auth-core'; +import type { Drizzle, Connection, Environments } from '@map-colonies/auth-core'; +import { clientTable, connectionTable, domainTable, Environment, keyTable } from '@map-colonies/auth-core'; import { faker } from '@faker-js/faker'; -import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { getRealKeys, getFakeClient, getFakeConnection, getFakeIConnection } from 'test-utils'; +import type { ExpectResponseStatus, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { expectResponseStatusFactory } from '@map-colonies/openapi-helpers/requestSender'; +import { getRealKeys, getFakeClient, getFakeConnection } from 'test-utils'; import type { paths, operations } from 'auth-openapi'; -import { getApp } from '@src/app.js'; -import { SERVICES } from '@common/constants.js'; -import type { ConnectionRepository } from '@src/connection/DAL/connectionRepository.js'; -import type { KeyRepository } from '@src/key/DAL/keyRepository.js'; -import type { DomainRepository } from '@src/domain/DAL/domainRepository.js'; -import { initConfig } from '@common/config.js'; -import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; +import { ConnectionRepository } from '@src/connection/DAL/connectionRepository.js'; +import { KeyRepository } from '@src/key/DAL/keyRepository.js'; +import { DomainRepository } from '@src/domain/DAL/domainRepository.js'; +import { initEnvironment } from '../setup.js'; + +const expectResponseStatus: ExpectResponseStatus = expectResponseStatusFactory(expect); describe('connection', function () { let requestSender: RequestSender; let depContainer: DependencyContainer; + let drizzle: Drizzle; const clients = [getFakeClient(false), getFakeClient(false)]; const connections = [ - { ...getFakeIConnection(), name: clients[0]!.name, environment: Environment.NP, domains: ['test'] }, - { ...getFakeIConnection(), name: clients[0]!.name, environment: Environment.PRODUCTION }, - { ...getFakeIConnection(), name: clients[0]!.name, environment: Environment.PRODUCTION, version: 2 }, - { ...getFakeIConnection(), name: clients[1]!.name, environment: Environment.NP }, + { ...getFakeConnection(), name: clients[0]!.name, environment: Environment.NP, domains: ['test'] }, + { ...getFakeConnection(), name: clients[0]!.name, environment: Environment.PROD }, + { ...getFakeConnection(), name: clients[0]!.name, environment: Environment.PROD, version: 2 }, + { ...getFakeConnection(), name: clients[1]!.name, environment: Environment.NP }, ]; beforeAll(async function () { - await initConfig(true); - const [app, container] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender(OPENAPI_PATH, app); - depContainer = container; + const env = await initEnvironment(); + depContainer = env.container; + requestSender = env.requestSender; + drizzle = env.drizzle; + await drizzle.insert(domainTable).values([{ name: 'alpha' }, { name: 'bravo' }, { name: 'test' }]); }); beforeEach(async function () { - await depContainer.resolve(DataSource).getRepository(Client).save(clients); - await depContainer.resolve(DataSource).getRepository(Connection).save(connections); - await depContainer - .resolve(DataSource) - .getRepository(Domain) - .save([{ name: 'alpha' }, { name: 'bravo' }, { name: 'test' }]); + // await depContainer.resolve(DataSource).getRepository(Client).save(clients); + // await depContainer.resolve(DataSource).getRepository(Connection).save(connections); + // await depContainer + // .resolve(DataSource) + // .getRepository(Domain) + // .save([{ name: 'alpha' }, { name: 'bravo' }, { name: 'test' }]); + const clientQuery = drizzle.insert(clientTable).values(clients); + const connectionQuery = drizzle.insert(connectionTable).values(connections); + await Promise.all([clientQuery, connectionQuery]); }); afterEach(async function () { - await depContainer.resolve(DataSource).getRepository(Connection).clear(); - await depContainer.resolve(DataSource).getRepository(Client).clear(); - await depContainer.resolve(DataSource).getRepository(Domain).clear(); - }); - - afterAll(async function () { - await depContainer.resolve(DataSource).destroy(); + // await depContainer.resolve(DataSource).getRepository(Connection).clear(); + // await depContainer.resolve(DataSource).getRepository(Client).clear(); + // await depContainer.resolve(DataSource).getRepository(Domain).clear(); + await Promise.all([drizzle.delete(connectionTable), drizzle.delete(clientTable)]); }); describe('Happy Path', function () { @@ -80,15 +72,17 @@ describe('connection', function () { it.each([ { name: 'avi', searchParam: 'avi', matchType: 'exact' }, - { name: 'bobavi', searchParam: 'avi', matchType: 'suffix' }, - { name: 'aviiiiii', searchParam: 'av', matchType: 'prefix' }, - { name: 'blaviabla', searchParam: 'avi', matchType: 'middle' }, - { name: 'avi', searchParam: 'AV', matchType: 'case-insensitive' }, + // { name: 'bobavi', searchParam: 'avi', matchType: 'suffix' }, + // { name: 'aviiiiii', searchParam: 'av', matchType: 'prefix' }, + // { name: 'blaviabla', searchParam: 'avi', matchType: 'middle' }, + // { name: 'avi', searchParam: 'AV', matchType: 'case-insensitive' }, ])('should find the connection of $name with search string $searchParam with match type $matchType', async function ({ name, searchParam }) { - const client = { ...getFakeClient(false), name }; - const connection = getFakeIConnection(); - connection.name = client.name; - await depContainer.resolve(DataSource).getRepository(Client).insert(client); + const client = { ...getFakeClient(false, { name }) }; + const connection = getFakeConnection(false, { name: client.name }); + // connection.name = client.name; + // await depContainer.resolve(DataSource).getRepository(Client).insert(client); + // await requestSender.upsertConnection({ requestBody: connection }); + await drizzle.insert(clientTable).values(client); await requestSender.upsertConnection({ requestBody: connection }); const res = await requestSender.getConnections({ @@ -104,10 +98,9 @@ describe('connection', function () { }); it('should return empty array when no connections match the client name search param', async function () { - const client = { ...getFakeClient(false), name: 'bla' }; - const connection = getFakeIConnection(); - connection.name = client.name; - await depContainer.resolve(DataSource).getRepository(Client).insert(client); + const client = { ...getFakeClient(false, { name: 'bla' }) }; + const connection = getFakeConnection(false, { name: client.name }); + await drizzle.insert(clientTable).values(client); await requestSender.upsertConnection({ requestBody: connection }); const res = await requestSender.getConnections({ @@ -116,9 +109,8 @@ describe('connection', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(0); }); @@ -130,17 +122,16 @@ describe('connection', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(3); }); it('should return latest connections for multiple clients', async function () { const client = { ...getFakeClient(false) }; - const connection = getFakeIConnection(); - connection.name = client.name; - await depContainer.resolve(DataSource).getRepository(Client).insert(client); + const connection = getFakeConnection(false, { name: client.name }); + // await depContainer.resolve(DataSource).getRepository(Client).insert(client); + await drizzle.insert(clientTable).values(client); await requestSender.upsertConnection({ requestBody: connection }); await requestSender.upsertConnection({ requestBody: connection }); @@ -150,17 +141,16 @@ describe('connection', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(4); }); it('should return latest connections with multiple query params', async function () { const client = { ...getFakeClient(false) }; - const connection = getFakeIConnection(); - connection.name = client.name; - await depContainer.resolve(DataSource).getRepository(Client).insert(client); + const connection = getFakeConnection(false, { name: client.name }); + // await depContainer.resolve(DataSource).getRepository(Client).insert(client); + await drizzle.insert(clientTable).values(client); await requestSender.upsertConnection({ requestBody: connection }); await requestSender.upsertConnection({ requestBody: connection }); @@ -174,43 +164,40 @@ describe('connection', function () { }, }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.items).toBeArrayOfSize(1); }); it('should return 200 status code and all the connections with specific env', async function () { - const res = await requestSender.getConnections({ queryParams: { environment: [Environment.PRODUCTION] } }); + const res = await requestSender.getConnections({ queryParams: { environment: [Environment.PROD] } }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - const returnedItems = res.body.items as IConnection[]; + const returnedItems = res.body.items; - expect(returnedItems).toSatisfyAll((c: IConnection) => c.environment.includes(Environment.PRODUCTION)); + expect(returnedItems).toSatisfyAll((c: Connection) => c.environment.includes(Environment.PROD)); }); it('should return 200 status code and all the connections with specific domain', async function () { const res = await requestSender.getConnections({ queryParams: { domains: ['test'] } }); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - const returnedItems = res.body.items as IConnection[]; + const returnedItems = res.body.items; - expect(returnedItems).toSatisfyAll((c: IConnection) => c.domains.includes('test')); + expect(returnedItems).toSatisfyAll((c: Connection) => c.domains.includes('test')); }); }); describe('POST /connection', function () { it('should return 201 status code and the created connection', async function () { const client = getFakeClient(false); - const connection = getFakeIConnection(); - connection.name = client.name; - await depContainer.resolve(DataSource).getRepository(Client).save(client); + const connection = getFakeConnection(false, { name: client.name }); + // await depContainer.resolve(DataSource).getRepository(Client).save(client); + await drizzle.insert(clientTable).values(client); const res = await requestSender.upsertConnection({ requestBody: connection }); @@ -224,10 +211,11 @@ describe('connection', function () { it('should return 200 status code and the updated connection', async function () { const client = getFakeClient(false); - const connection = getFakeIConnection(); - connection.name = client.name; - await depContainer.resolve(DataSource).getRepository(Client).save(client); - await depContainer.resolve(DataSource).getRepository(Connection).save(connection); + const connection = getFakeConnection(false, { name: client.name }); + // await depContainer.resolve(DataSource).getRepository(Client).save(client); + // await depContainer.resolve(DataSource).getRepository(Connection).save(connection); + await drizzle.insert(clientTable).values(client); + await drizzle.insert(connectionTable).values(connection); delete connection.createdAt; @@ -240,10 +228,11 @@ describe('connection', function () { it('should not generate a token and return an empty string if no token is supplied and no private key is available and ignoreErrors is true', async function () { const client = getFakeClient(false); - const connection = getFakeIConnection(); - connection.name = client.name; - connection.token = ''; - await depContainer.resolve(DataSource).getRepository(Client).save(client); + const connection = getFakeConnection(false, { name: client.name, token: '' }); + // connection.name = client.name; + // connection.token = ''; + // await depContainer.resolve(DataSource).getRepository(Client).save(client); + await drizzle.insert(clientTable).values(client); const res = await requestSender.upsertConnection({ requestBody: connection, queryParams: { shouldIgnoreTokenErrors: true } }); delete connection.createdAt; @@ -255,15 +244,18 @@ describe('connection', function () { it('should generate a token if no token is supplied and private key is available', async function () { const client = getFakeClient(false); - const connection = getFakeIConnection(); + const connection = getFakeConnection(false, { name: client.name, environment: Environment.STAGE }); const keys = getRealKeys(); - connection.name = client.name; - connection.environment = Environment.STAGE; - await depContainer.resolve(DataSource).getRepository(Client).save(client); - await depContainer.resolve(DataSource).getRepository(Connection).save(connection); - const keyRepo = depContainer.resolve(DataSource).getRepository(Key); - await keyRepo.clear(); - await keyRepo.save({ environment: connection.environment, version: 1, privateKey: keys[0], publicKey: keys[1] }); + // connection.name = client.name; + // connection.environment = Environment.STAGE; + // await depContainer.resolve(DataSource).getRepository(Client).save(client); + // await depContainer.resolve(DataSource).getRepository(Connection).save(connection); + await drizzle.insert(clientTable).values(client); + await drizzle.insert(connectionTable).values(connection); + // const keyRepo = depContainer.resolve(DataSource).getRepository(Key); + // await keyRepo.clear(); + // await keyRepo.save({ environment: connection.environment, version: 1, privateKey: keys[0], publicKey: keys[1] }); + await drizzle.insert(keyTable).values({ environment: connection.environment, version: 1, privateKey: keys[0], publicKey: keys[1] }); delete connection.createdAt; @@ -276,10 +268,11 @@ describe('connection', function () { it('should create a connection with origins sorted with asterisk strings last', async function () { const client = getFakeClient(false); - const connection = getFakeIConnection(); - connection.name = client.name; - connection.origins = ['http://example.com', 'https://*.test.com', 'http://foo.com']; - await depContainer.resolve(DataSource).getRepository(Client).save(client); + const connection = getFakeConnection(false, { name: client.name, origins: ['http://example.com', 'https://*.test.com', 'http://foo.com'] }); + // connection.name = client.name; + // connection.origins = ['http://example.com', 'https://*.test.com', 'http://foo.com']; + // await depContainer.resolve(DataSource).getRepository(Client).save(client); + await drizzle.insert(clientTable).values(client); const res = await requestSender.upsertConnection({ requestBody: connection }); @@ -295,19 +288,19 @@ describe('connection', function () { expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toSatisfyAll((c: IConnection) => c.name === connections[0]!.name); + expect(res.body).toSatisfyAll((c: Connection) => c.name === connections[0]!.name); }); }); describe('GET /client/:clientName/connection/:environment', function () { it('should return 200 status code all the connections with the specific name', async function () { const res = await requestSender.getClientEnvironmentConnections({ - pathParams: { clientName: connections[0]!.name, environment: Environment.PRODUCTION }, + pathParams: { clientName: connections[0]!.name, environment: Environment.PROD }, }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toSatisfyAll((c: IConnection) => c.name === connections[0]!.name && c.environment === Environment.PRODUCTION); + expect(res.body).toSatisfyAll((c: Connection) => c.name === connections[0]!.name && c.environment === Environment.PROD); }); }); @@ -325,17 +318,16 @@ describe('connection', function () { describe('GET /client/:clientName/connection/:environment/latest', function () { it('should return 200 status code and the latest connection for the environment', async function () { - const expectedConnection = connections.find( - (c) => c.name === connections[0]!.name && c.environment === Environment.PRODUCTION && c.version === 2 - ); + const expectedConnection = connections.find((c) => c.name === connections[0]!.name && c.environment === Environment.PROD && c.version === 2); const res = await requestSender.getClientLatestConnection({ - pathParams: { clientName: connections[0]!.name, environment: Environment.PRODUCTION }, + pathParams: { clientName: connections[0]!.name, environment: Environment.PROD }, }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toStrictEqual({ ...expectedConnection, createdAt: expectedConnection!.createdAt?.toISOString() }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect(res.body).toStrictEqual({ ...expectedConnection, createdAt: expect.any(String) }); }); it('should return 200 status code and the only connection when there is only one version', async function () { @@ -347,24 +339,25 @@ describe('connection', function () { expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toStrictEqual({ ...expectedConnection, createdAt: expectedConnection!.createdAt?.toISOString() }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect(res.body).toStrictEqual({ ...expectedConnection, createdAt: expect.any(String) }); }); }); }); describe('Bad Path', function () { describe('POST /connection', function () { - it('should return 400 if the request body is incorrect', async function () { - const connection = getFakeConnection(); - connection.domains = []; + it.only('should return 400 if the request body is incorrect', async function () { + const connection = getFakeConnection(false, { name: clients[0]!.name, domains: [] }); const res = await requestSender.upsertConnection({ requestBody: connection }); + console.log(res.body); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); expect(res).toSatisfyApiSpec(); }); it('should return 400 if a domain is not in the DB', async function () { - const connection = getFakeConnection(); + const connection = getFakeConnection(false, { name: clients[0]!.name }); connection.domains = ['c']; const res = await requestSender.upsertConnection({ requestBody: connection }); @@ -373,17 +366,18 @@ describe('connection', function () { }); it('should return 400 if token generation failed because of missing private key', async function () { - const connection = getFakeConnection(); + const connection = getFakeConnection(false, { name: clients[0]!.name }); connection.token = ''; const res = await requestSender.upsertConnection({ requestBody: connection }); + console.log(res.body); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); expect(res).toSatisfyApiSpec(); }); it('should return 404 if a client with name is not in the DB', async function () { - const connection = getFakeIConnection(); - connection.name = faker.string.alpha(5); + const connection = getFakeConnection(false, { name: faker.string.alpha(5) }); + // connection.name = faker.string.alpha(5); const res = await requestSender.upsertConnection({ requestBody: connection }); expect(res).toHaveProperty('status', httpStatusCodes.NOT_FOUND); @@ -400,9 +394,10 @@ describe('connection', function () { it("should return 409 if the no connection exists and request version isn't 1", async function () { const client = getFakeClient(false); - await depContainer.resolve(DataSource).getRepository(Client).save(client); + // await depContainer.resolve(DataSource).getRepository(Client).save(client); + await drizzle.insert(clientTable).values(client); - const res = await requestSender.upsertConnection({ requestBody: { ...getFakeIConnection(), name: client.name, version: 2 } }); + const res = await requestSender.upsertConnection({ requestBody: { ...getFakeConnection(), name: client.name, version: 2 } }); expect(res).toHaveProperty('status', httpStatusCodes.CONFLICT); expect(res).toSatisfyApiSpec(); @@ -486,12 +481,13 @@ describe('connection', function () { describe('GET /connection', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); - const qbMock = { - getMany: vi.fn().mockRejectedValue(new Error('DB Error')), - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - vi.spyOn(repo, 'createQueryBuilder').mockReturnValue(qbMock as any); + // const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); + // const qbMock = { + // getMany: vi.fn().mockRejectedValue(new Error('DB Error')), + // }; + // // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + // vi.spyOn(repo, 'createQueryBuilder').mockReturnValue(qbMock as any); + vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getConnections(); @@ -502,10 +498,10 @@ describe('connection', function () { describe('POST /connection', function () { it('should return 500 status code if db throws an error', async function () { - const connection = getFakeIConnection(); + const connection = getFakeConnection(); connection.name = clients[0]!.name; - const repo = depContainer.resolve(SERVICES.DOMAIN_REPOSITORY); + const repo = depContainer.resolve(DomainRepository); vi.spyOn(repo, 'checkInputForNonExistingDomains').mockRejectedValue(new Error()); const res = await requestSender.upsertConnection({ requestBody: connection }); @@ -515,13 +511,14 @@ describe('connection', function () { }); it('should return 500 if token generation fails', async function () { - const connection = getFakeIConnection(); + const connection = getFakeConnection(); connection.name = clients[0]!.name; connection.token = ''; - const keyRepo = depContainer.resolve(SERVICES.KEY_REPOSITORY); + const keyRepo = depContainer.resolve(KeyRepository); vi.spyOn(keyRepo, 'getLatestKeys').mockRejectedValue(new Error()); const res = await requestSender.upsertConnection({ requestBody: connection }); + console.log(res.body); expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); expect(res).toSatisfyApiSpec(); @@ -530,12 +527,11 @@ describe('connection', function () { describe('GET /client/:clientName/connection', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); - const qbMock = { - getMany: vi.fn().mockRejectedValue(new Error('DB Error')), - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - vi.spyOn(repo, 'createQueryBuilder').mockReturnValue(qbMock as any); + // const repo = depContainer.resolve(ConnectionRepository); + // const qbMock = { + // getMany: vi.fn().mockRejectedValue(new Error('DB Error')), + // }; + vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getClientConnections({ pathParams: { clientName: 'avi' } }); @@ -546,12 +542,11 @@ describe('connection', function () { describe('GET /client/:clientName/connection/:environment', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); - const qbMock = { - getMany: vi.fn().mockRejectedValue(new Error('DB Error')), - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - vi.spyOn(repo, 'createQueryBuilder').mockReturnValue(qbMock as any); + // const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); + // const qbMock = { + // getMany: vi.fn().mockRejectedValue(new Error('DB Error')), + // }; + vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getClientEnvironmentConnections({ pathParams: { clientName: 'avi', environment: Environment.NP } }); @@ -562,13 +557,14 @@ describe('connection', function () { describe('GET /client/:clientName/connection/:environment/:version', function () { it('should return 500 status code if db throws an error', async function () { - const connection = getFakeIConnection(); + const connection = getFakeConnection(); connection.name = clients[0]!.name; connection.environment = Environment.NP; await requestSender.upsertConnection({ requestBody: connection }); - const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); - vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + // const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); + // vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.connection, 'findFirst').mockRejectedValue(new Error()); const res = await requestSender.getClientVersionedConnection({ pathParams: { clientName: clients[0]!.name, environment: Environment.NP, version: 1 }, @@ -581,7 +577,7 @@ describe('connection', function () { describe('GET /client/:clientName/connection/:environment/latest', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); + const repo = depContainer.resolve(ConnectionRepository); vi.spyOn(repo, 'getMaxVersion').mockRejectedValue(new Error()); const res = await requestSender.getClientLatestConnection({ diff --git a/apps/auth-manager/tests/integration/domain/domain.spec.mts b/apps/auth-manager/tests/integration/domain/domain.spec.mts index 72fc04df..7c32e688 100644 --- a/apps/auth-manager/tests/integration/domain/domain.spec.mts +++ b/apps/auth-manager/tests/integration/domain/domain.spec.mts @@ -8,49 +8,39 @@ import 'jest-openapi'; import { Pool } from 'pg'; import type { Drizzle } from '@map-colonies/auth-core'; import { domainTable } from '@map-colonies/auth-core'; -import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import type { ExpectResponseStatus, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { createRequestSender, expectResponseStatusFactory } from '@map-colonies/openapi-helpers/requestSender'; import type { paths, operations } from 'auth-openapi'; import { getApp } from '@src/app.js'; import { SERVICES } from '@src/common/constants.js'; import { initConfig } from '@src/common/config.js'; import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; +import { initEnvironment } from '../setup.js'; + +const expectResponseStatus: ExpectResponseStatus = expectResponseStatusFactory(expect); describe('domain', function () { let requestSender: RequestSender; - let depContainer: DependencyContainer; + let drizzle: Drizzle; beforeAll(async function () { - await initConfig(true); - - const [app, container] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender(OPENAPI_PATH, app); - depContainer = container; - }); - - afterAll(async function () { - await depContainer.resolve(Pool).end(); + const env = await initEnvironment(); + requestSender = env.requestSender; + drizzle = env.drizzle; }); describe('Happy Path', function () { describe('GET /domain', function () { it('should return 200 status code and a list of domains', async function () { - const drizzle = depContainer.resolve(SERVICES.DRIZZLE); + // const drizzle = depContainer.resolve(SERVICES.DRIZZLE); await drizzle.insert(domainTable).values([{ name: 'avi' }, { name: 'iva' }]); const res = await requestSender.getDomains(); - expect(res).toHaveProperty('status', httpStatusCodes.OK); + expectResponseStatus(res, httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly - const returnedItems = res.body.items as IDomain[]; + const returnedItems = res.body.items; expect(returnedItems).toEqual(expect.arrayContaining([{ name: 'avi' }, { name: 'iva' }])); }); diff --git a/apps/auth-manager/tests/integration/setup.ts b/apps/auth-manager/tests/integration/setup.ts new file mode 100644 index 00000000..c6ebccbc --- /dev/null +++ b/apps/auth-manager/tests/integration/setup.ts @@ -0,0 +1,35 @@ +import { jsLogger } from '@map-colonies/js-logger'; +import { trace } from '@opentelemetry/api'; +import { afterAll } from 'vitest'; +import type { DependencyContainer } from 'tsyringe'; +import { Pool } from 'pg'; +import type { Drizzle } from '@map-colonies/auth-core'; +import { createRequestSender, type RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import type { operations, paths } from 'auth-openapi'; +import { getApp } from '@src/app'; +import { initConfig } from '@src/common/config'; +import { SERVICES } from '@src/common/constants'; +import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; + +export async function initEnvironment(): Promise<{ + container: DependencyContainer; + requestSender: RequestSender; + drizzle: Drizzle; +}> { + await initConfig(true); + const [app, container] = await getApp({ + override: [ + { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + ], + useChild: true, + }); + const requestSender = await createRequestSender(OPENAPI_PATH, app); + const drizzle = container.resolve(SERVICES.DRIZZLE); + + afterAll(async function () { + await container.resolve(Pool).end(); + }); + + return { container, requestSender, drizzle }; +} diff --git a/packages/auth-core/src/db/entities/common.ts b/packages/auth-core/src/db/entities/common.ts index 8cd1ff2a..e3977b43 100644 --- a/packages/auth-core/src/db/entities/common.ts +++ b/packages/auth-core/src/db/entities/common.ts @@ -5,3 +5,5 @@ export const authManagerSchema = d.snakeCase.schema('auth_manager'); export const environmentEnum = authManagerSchema.enum('environment_enum', ['np', 'stage', 'prod']); export const createdAtColumn = timestamp({ withTimezone: true }).defaultNow().notNull(); + +export type Environments = (typeof environmentEnum.enumValues)[number]; diff --git a/packages/auth-core/src/db/utils/createConnection.ts b/packages/auth-core/src/db/utils/createConnection.ts index 75dd310b..7c90ba19 100644 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ b/packages/auth-core/src/db/utils/createConnection.ts @@ -28,6 +28,8 @@ export async function initConnection(dbConfig: commonDbFullV1Type): Promise>; +export type DrizzleTx = Parameters[0]>[0]; + export function createDrizzle(pool: Pool): Drizzle { return drizzle({ client: pool, relations }); } diff --git a/packages/test-utils/src/fakers.ts b/packages/test-utils/src/fakers.ts index db3a952f..8a7c9ea6 100644 --- a/packages/test-utils/src/fakers.ts +++ b/packages/test-utils/src/fakers.ts @@ -31,10 +31,10 @@ export function getFakeAsset(includeCreated?: boolean): Asset | NewAsset { }; } -export function getFakeConnection(includeCreated: true): Connection; -export function getFakeConnection(includeCreated?: false): NewConnection; -export function getFakeConnection(includeCreated?: boolean): Connection | NewConnection { - return { +export function getFakeConnection(includeCreated: true, override?: Partial): Connection; +export function getFakeConnection(includeCreated?: false, override?: Partial): NewConnection; +export function getFakeConnection(includeCreated?: boolean, override?: Partial): Connection | NewConnection { + const connection: Connection | NewConnection = { createdAt: includeCreated === true ? faker.date.past() : undefined, environment: Environment.NP, version: 1, @@ -46,6 +46,7 @@ export function getFakeConnection(includeCreated?: boolean): Connection | NewCon enabled: true, token: faker.string.alpha(), }; + return { ...connection, ...override }; } export function getFakeBundle(includeCreated: true): Bundle; @@ -64,9 +65,9 @@ export function getFakeBundle(includeCreated?: boolean): Bundle | NewBundle { }; } -export function getFakeClient(includeGeneratedFields: true): Client; -export function getFakeClient(includeGeneratedFields?: false): NewClient; -export function getFakeClient(includeGeneratedFields?: boolean): NewClient | Client { +export function getFakeClient(includeGeneratedFields: true, override?: Partial): Client; +export function getFakeClient(includeGeneratedFields?: false, override?: Partial): NewClient; +export function getFakeClient(includeGeneratedFields?: boolean, override?: Partial): NewClient | Client { const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); const client: NewClient = { @@ -91,7 +92,7 @@ export function getFakeClient(includeGeneratedFields?: boolean): NewClient | Cli client.createdAt = faker.date.past(); client.updatedAt = faker.date.between({ from: client.createdAt, to: Date.now() }); } - return client; + return { ...client, ...override }; } export function getMockKeys(): [JWKPrivateKey, JWKPublicKey] { From c8d5f9e9430635790802027411dc9d1a7ce56bca Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Sun, 24 May 2026 13:23:51 +0300 Subject: [PATCH 07/14] refactor(auth-manager): migrate from TypeORM to Drizzle --- apps/auth-manager/dataSource.mjs | 31 ----- apps/auth-manager/package.json | 1 - .../src/asset/DAL/assetRepository.ts | 2 +- .../src/asset/models/assetManager.ts | 3 - .../src/bundle/models/bundleManager.ts | 3 - .../src/client/models/clientManager.ts | 40 +------ apps/auth-manager/src/common/db/pagination.ts | 11 +- apps/auth-manager/src/common/db/utils.ts | 6 +- .../connection/models/connectionManager.ts | 16 +-- apps/auth-manager/src/containerConfig.ts | 8 +- .../src/domain/DAL/domainRepository.ts | 4 +- .../src/domain/models/domainManager.ts | 4 +- .../auth-manager/src/key/DAL/keyRepository.ts | 2 +- .../auth-manager/src/key/models/keyManager.ts | 27 ----- .../tests/integration/bundle/bundle.spec.mts | 2 - .../tests/integration/client/client.spec.mts | 20 ---- .../connection/connection.spec.mts | 28 ++--- .../tests/integration/docs/docs.spec.ts | 5 +- .../tests/integration/domain/domain.spec.mts | 5 +- .../tests/integration/key/key.spec.mts | 110 +++++++----------- .../unit/asset/models/assetManager.spec.mts | 61 +++++----- .../unit/client/models/clientManager.spec.mts | 27 +++-- .../models/connectionManager.spec.mts | 102 ++++++---------- packages/auth-core/package.json | 9 +- packages/auth-openapi/openapi3.yaml | 2 +- 25 files changed, 163 insertions(+), 366 deletions(-) delete mode 100644 apps/auth-manager/dataSource.mjs diff --git a/apps/auth-manager/dataSource.mjs b/apps/auth-manager/dataSource.mjs deleted file mode 100644 index 3965a0c0..00000000 --- a/apps/auth-manager/dataSource.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { DataSource } from 'typeorm'; -import { createConnectionOptions } from '@map-colonies/auth-core'; -/** - * - * @param {string} moduleName - * @returns {Promise} - */ -async function importModule(moduleName) { - let imported; - try { - imported = await import(`./${moduleName}`); - } catch (err) { - if (err instanceof Error && 'code' in err && err.code === 'ERR_MODULE_NOT_FOUND') { - imported = await import(`./dist/${moduleName}`); - } else { - throw err; - } - } - return imported; -} -await importModule('instrumentation.mjs'); - -const { getConfig } = await importModule('common/config.js'); -const configOption = getConfig().get('db'); -const connectionOptions = configOption; - -const appDataSource = new DataSource({ - ...createConnectionOptions(connectionOptions), -}); - -export default appDataSource; diff --git a/apps/auth-manager/package.json b/apps/auth-manager/package.json index 5fdc68a1..4bc76c91 100644 --- a/apps/auth-manager/package.json +++ b/apps/auth-manager/package.json @@ -80,7 +80,6 @@ "jest-extended": "catalog:", "jest-openapi": "catalog:", "supertest": "catalog:", - "type-fest": "^4.40.0", "typescript": "catalog:", "vitest": "catalog:", "@map-colonies/vitest-utils": "catalog:", diff --git a/apps/auth-manager/src/asset/DAL/assetRepository.ts b/apps/auth-manager/src/asset/DAL/assetRepository.ts index 80017d34..7f80ddd0 100644 --- a/apps/auth-manager/src/asset/DAL/assetRepository.ts +++ b/apps/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,6 +1,6 @@ import { and, eq, max } from 'drizzle-orm'; import { assetTable, type DrizzleTx, type Drizzle } from '@map-colonies/auth-core'; -import { inject, injectable, Lifecycle, scoped } from 'tsyringe'; +import { inject, Lifecycle, scoped } from 'tsyringe'; import { SERVICES } from '@common/constants'; @scoped(Lifecycle.ContainerScoped) diff --git a/apps/auth-manager/src/asset/models/assetManager.ts b/apps/auth-manager/src/asset/models/assetManager.ts index 8f806ae9..3ebb6697 100644 --- a/apps/auth-manager/src/asset/models/assetManager.ts +++ b/apps/auth-manager/src/asset/models/assetManager.ts @@ -19,7 +19,6 @@ export class AssetManager { this.logger.info({ msg: 'fetching assets', searchParams }); const { environment, isTemplate, type } = searchParams; - // return this.assetRepository.findBy({ environment: environment ? ArrayContains(environment) : undefined, isTemplate, type }); return this.drizzle.query.asset.findMany({ where: { isTemplate, @@ -32,14 +31,12 @@ export class AssetManager { public async getNamedAssets(name: string): Promise { this.logger.info({ msg: 'fetching all specific environment assets', asset: { name } }); - // return this.assetRepository.findBy({ name }); return this.drizzle.query.asset.findMany({ where: { name } }); } public async getAsset(name: string, version: number): Promise { this.logger.info({ msg: 'fetching asset', asset: { name, version } }); - // const asset = await this.assetRepository.findOne({ where: { name, version } }); const asset = await this.drizzle.query.asset.findFirst({ where: { name, version } }); if (asset === undefined) { diff --git a/apps/auth-manager/src/bundle/models/bundleManager.ts b/apps/auth-manager/src/bundle/models/bundleManager.ts index 8c9edca9..6c8bb6fc 100644 --- a/apps/auth-manager/src/bundle/models/bundleManager.ts +++ b/apps/auth-manager/src/bundle/models/bundleManager.ts @@ -1,7 +1,6 @@ import { type Logger } from '@map-colonies/js-logger'; import type { Bundle, Drizzle } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -// import { In, Repository } from 'typeorm'; import { SERVICES } from '@common/constants'; import { BundleSearchParams } from './bundle'; import { BundleNotFoundError } from './errors'; @@ -21,7 +20,6 @@ export class BundleManager { return this.drizzle.query.bundle.findMany({ where: { - // createdAt: createDatesComparison(createdAfter, createdBefore), environment: environment ? { in: environment } : undefined, createdAt: { gte: createdAfter, lte: createdBefore }, }, @@ -31,7 +29,6 @@ export class BundleManager { public async getBundle(id: number): Promise { this.logger.info({ msg: 'fetching bundle', id }); - // const bundle = await this.bundleRepository.findOneBy({ id }); const bundle = await this.drizzle.query.bundle.findFirst({ where: { id } }); if (bundle === undefined) { diff --git a/apps/auth-manager/src/client/models/clientManager.ts b/apps/auth-manager/src/client/models/clientManager.ts index 8c6d6e93..4269e2ab 100644 --- a/apps/auth-manager/src/client/models/clientManager.ts +++ b/apps/auth-manager/src/client/models/clientManager.ts @@ -1,22 +1,16 @@ import { type Logger } from '@map-colonies/js-logger'; import { inject, injectable } from 'tsyringe'; -// import { ArrayContains, ILike, QueryFailedError } from 'typeorm'; import { DatabaseError } from 'pg'; -import { count, DrizzleQueryError, eq, and, arrayContains, asc, desc, SQL, AnyColumn, ilike } from 'drizzle-orm'; +import { count, eq, and, arrayContains, ilike } from 'drizzle-orm'; import { clientTable, type Client, type Drizzle, type NewClient } from '@map-colonies/auth-core'; -import { PgTable } from 'drizzle-orm/pg-core'; import { SERVICES } from '@common/constants'; import { pgErrorCodes } from '@common/db/constants'; -import { createDatesComparison, sortOptionsToOrderBy } from '@common/db/utils'; +import { createDatesComparison, isDrizzleQueryError, sortOptionsToOrderBy } from '@common/db/utils'; import { SortOptions } from '@src/common/db/sort'; import { PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import { ClientAlreadyExistsError, ClientNotFoundError } from './errors'; import { ClientSearchParams } from './client'; -function isDrizzleQueryError(err: unknown): err is DrizzleQueryError { - return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'DrizzleQueryError'; -} - @injectable() export class ClientManager { public constructor( @@ -32,36 +26,7 @@ export class ClientManager { this.logger.info({ msg: 'fetching clients' }); this.logger.debug({ msg: 'search parameters', searchParams }); - // eslint doesn't recognize this as valid because its in the type definition - // let findOptions: Parameters[0] = {}; const { name, branch, tags, createdAfter, createdBefore, updatedAfter, updatedBefore } = searchParams; - // findOptions = { - // where: { - // name: name !== undefined ? ILike(`%${name}%`) : undefined, - // tags: tags ? ArrayContains(tags) : undefined, - // branch, - // createdAt: createDatesComparison(createdAfter, createdBefore), - // updatedAt: createDatesComparison(updatedAfter, updatedBefore), - // }, - // }; - - // if (paginationParams !== undefined) { - // findOptions = { - // ...findOptions, - // ...paginationParamsToFindOptions(paginationParams), - // }; - // } - - // if (sortParams !== undefined) { - // findOptions.order = sortParams; - // } - - // return this.clientRepository.findAndCount({ ...findOptions }); - - // let findOptions: Parameters[0] = { - // where: { - // }, - // }; const whereClause = and( name !== undefined ? ilike(clientTable.name, `%${name}%`) : undefined, @@ -112,7 +77,6 @@ export class ClientManager { return res[0] as Client; } catch (error) { - console.error(error); if (isDrizzleQueryError(error) && error.cause instanceof DatabaseError && error.cause.code === pgErrorCodes.UNIQUE_VIOLATION) { throw new ClientAlreadyExistsError('client already exists'); } diff --git a/apps/auth-manager/src/common/db/pagination.ts b/apps/auth-manager/src/common/db/pagination.ts index d6b4ace4..0497985e 100644 --- a/apps/auth-manager/src/common/db/pagination.ts +++ b/apps/auth-manager/src/common/db/pagination.ts @@ -9,7 +9,7 @@ export interface PaginationParams { export function paginationParamsToOffsetAndLimit(paginationParams?: PaginationParams): { limit: number; offset: number } { if (paginationParams === undefined) { - return {}; + return { limit: DEFAULT_PAGE_SIZE, offset: 0 }; } const { page, pageSize } = paginationParams; @@ -19,12 +19,3 @@ export function paginationParamsToOffsetAndLimit(paginationParams?: PaginationPa offset: (page - 1) * pageSize, }; } - -// TODO - move to generic package -export function withPagination(qb: T, orderByColumn: PgColumn | SQL | SQL.Aliased, params: PaginationParams): T { - const { page, pageSize } = params; - return qb - .orderBy(orderByColumn) - .limit(pageSize) - .offset((page - 1) * pageSize); -} diff --git a/apps/auth-manager/src/common/db/utils.ts b/apps/auth-manager/src/common/db/utils.ts index 195eddd4..6b766d5d 100644 --- a/apps/auth-manager/src/common/db/utils.ts +++ b/apps/auth-manager/src/common/db/utils.ts @@ -1,4 +1,4 @@ -import { between, lt, gt, type SQL, type Column, asc, type AnyColumn, desc, type Subquery } from 'drizzle-orm'; +import { between, lt, gt, type SQL, type Column, asc, type AnyColumn, desc, type Subquery, type DrizzleQueryError } from 'drizzle-orm'; import type { PgTable } from 'drizzle-orm/pg-core'; import type { SortOptions } from './sort'; @@ -28,3 +28,7 @@ export function sortOptionsToOrderBy(tableDefiniti } return result; } + +export function isDrizzleQueryError(err: unknown): err is DrizzleQueryError { + return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'DrizzleQueryError'; +} diff --git a/apps/auth-manager/src/connection/models/connectionManager.ts b/apps/auth-manager/src/connection/models/connectionManager.ts index 64ecda17..ce214adb 100644 --- a/apps/auth-manager/src/connection/models/connectionManager.ts +++ b/apps/auth-manager/src/connection/models/connectionManager.ts @@ -3,7 +3,7 @@ import { type Connection, connectionTable, type DrizzleTx, Environments, type Ne import { inject, injectable } from 'tsyringe'; import { JWK } from 'jose'; import { paths } from 'auth-openapi'; -import { ilike, SQL, inArray, eq, arrayContains, count, and, desc, asc, countDistinct, sql } from 'drizzle-orm'; +import { ilike, SQL, inArray, eq, arrayContains, count, and, desc, countDistinct, sql } from 'drizzle-orm'; import { ClientNotFoundError } from '@client/models/errors'; import { sortOptionsToOrderBy } from '@src/common/db/utils'; import { SERVICES } from '@common/constants'; @@ -100,7 +100,6 @@ export class ConnectionManager { const connections = await this.drizzle .select() .from(subQuery) - .where(filters) .orderBy(...sortOptionsToOrderBy(subQuery, sortParams ?? {})) .limit(limit) .offset(offset); @@ -111,7 +110,6 @@ export class ConnectionManager { public async getConnection(name: string, environment: Environments, version: number): Promise { this.logger.info({ msg: 'fetching connection', connection: { name, version, environment } }); - // const connection = await this.connectionRepository.findOne({ where: { name, version } }); const connection = await this.drizzle.query.connection.findFirst({ where: { name, version, environment } }); if (connection === undefined) { @@ -134,12 +132,8 @@ export class ConnectionManager { public async upsertConnection(connection: NewConnection, ignoreTokenErrors = false): Promise { this.logger.info({ msg: 'upserting connection', connection: { environment: connection.environment, version: connection.version } }); - // return this.connectionRepository.manager.transaction(async (transactionManager) => { - return this.drizzle.transaction(async (tx) => { - // const connectionRepo = transactionManager.withRepository(this.connectionRepository); - // const domainRepo = transactionManager.withRepository(this.domainRepository); - // const client = await transactionManager.getRepository(Client).findOneBy({ name: connection.name }); + return this.drizzle.transaction(async (tx) => { const client = await tx.query.client.findFirst({ where: { name: connection.name } }); if (client === undefined) { @@ -164,8 +158,8 @@ export class ConnectionManager { throw new ConnectionVersionMismatchError(msg); } this.logger.info({ msg: 'creating new connection', connection: { clientName: connection.name, environment: connection.environment } }); + // insert - // return connectionRepo.save(connection); return (await tx.insert(connectionTable).values(connection).returning())[0] as Connection; } @@ -180,8 +174,8 @@ export class ConnectionManager { // update return ( await tx - .update(connectionTable) - .set({ ...connection, version: maxVersion + 1 }) + .insert(connectionTable) + .values({ ...connection, version: maxVersion + 1 }) .returning() )[0] as Connection; }); diff --git a/apps/auth-manager/src/containerConfig.ts b/apps/auth-manager/src/containerConfig.ts index 8aca1b3c..b9908b79 100644 --- a/apps/auth-manager/src/containerConfig.ts +++ b/apps/auth-manager/src/containerConfig.ts @@ -3,10 +3,9 @@ import { trace } from '@opentelemetry/api'; import { instanceCachingFactory } from 'tsyringe'; import type { DependencyContainer } from 'tsyringe/dist/typings/types'; import { jsLogger } from '@map-colonies/js-logger'; -// import { DataSource } from 'typeorm'; import type { HealthCheck } from '@godaddy/terminus'; import { Pool } from 'pg'; -import { Bundle, createConnectionOptions, createDrizzle, initConnection } from '@map-colonies/auth-core'; +import { createDrizzle, initConnection } from '@map-colonies/auth-core'; import { Registry } from 'prom-client'; import { DB_CONNECTION_TIMEOUT, SERVICES, SERVICE_NAME } from './common/constants'; import { domainRouterFactory, DOMAIN_ROUTER_SYMBOL } from './domain/routes/domainRouter'; @@ -14,14 +13,9 @@ import type { InjectionObject } from './common/dependencyRegistration'; import { registerDependencies } from './common/dependencyRegistration'; import { promiseTimeout } from './common/utils/promiseTimeout'; import { clientRouterFactory, CLIENT_ROUTER_SYMBOL } from './client/routes/clientRouter'; -import { clientRepositoryFactory } from './client/DAL/clientRepository'; -import { keyRepositoryFactory } from './key/DAL/keyRepository'; import { keyRouterFactory, KEY_ROUTER_SYMBOL } from './key/routes/keyRouter'; import { assetRouterFactory, ASSET_ROUTER_SYMBOL } from './asset/routes/assetRouter'; -import { assetRepositoryFactory } from './asset/DAL/assetRepository'; -import { connectionRepositoryFactory } from './connection/DAL/connectionRepository'; import { connectionRouterFactory, CONNECTION_ROUTER_SYMBOL } from './connection/routes/connectionRouter'; -// import { domainRepositoryFactory } from './domain/DAL/domainRepository'; import { bundleRouterFactory, BUNDLE_ROUTER_SYMBOL } from './bundle/routes/bundleRouter'; import { getConfig } from './common/config'; import { getTracing } from './common/tracing'; diff --git a/apps/auth-manager/src/domain/DAL/domainRepository.ts b/apps/auth-manager/src/domain/DAL/domainRepository.ts index 44d3f0a3..7f92e9c6 100644 --- a/apps/auth-manager/src/domain/DAL/domainRepository.ts +++ b/apps/auth-manager/src/domain/DAL/domainRepository.ts @@ -1,9 +1,9 @@ import { inArray } from 'drizzle-orm'; import { domainTable, type Drizzle } from '@map-colonies/auth-core'; -import { inject, injectable } from 'tsyringe'; +import { inject, Lifecycle, scoped } from 'tsyringe'; import { SERVICES } from '@common/constants'; -@injectable() +@scoped(Lifecycle.ContainerScoped) export class DomainRepository { public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} diff --git a/apps/auth-manager/src/domain/models/domainManager.ts b/apps/auth-manager/src/domain/models/domainManager.ts index 91009b3f..24b46174 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -4,6 +4,7 @@ import { inject, injectable } from 'tsyringe'; import { count } from 'drizzle-orm'; import { type PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import type { SortOptions } from '@src/common/db/sort'; +import { isDrizzleQueryError } from '@src/common/db/utils'; import { SERVICES } from '@common/constants'; import { DomainAlreadyExistsError } from './errors'; @@ -32,7 +33,6 @@ export class DomainManager { const result = await Promise.all([domainsQuery, countQuery]); return [result[0], result[1][0]?.count ?? 0]; - // return this.domainRepository.findAndCount(findOptions); } public async createDomain(domain: NewDomain): Promise { @@ -41,7 +41,7 @@ export class DomainManager { await this.drizzle.insert(domainTable).values(domain); return domain; } catch (error) { - if (error instanceof Error && error.message.includes('duplicate key value violates unique constraint')) { + if (isDrizzleQueryError(error) && (error.cause?.message.includes('duplicate key value violates unique constraint') ?? false)) { throw new DomainAlreadyExistsError('domain already exists'); } throw error; diff --git a/apps/auth-manager/src/key/DAL/keyRepository.ts b/apps/auth-manager/src/key/DAL/keyRepository.ts index 206a6ffc..62d8ae59 100644 --- a/apps/auth-manager/src/key/DAL/keyRepository.ts +++ b/apps/auth-manager/src/key/DAL/keyRepository.ts @@ -1,6 +1,6 @@ import { and, eq, max } from 'drizzle-orm'; import { keyTable, type Key, type Drizzle, type DrizzleTx } from '@map-colonies/auth-core'; -import { inject, injectable, Lifecycle, scoped } from 'tsyringe'; +import { inject, Lifecycle, scoped } from 'tsyringe'; import { SERVICES } from '@common/constants'; @scoped(Lifecycle.ContainerScoped) diff --git a/apps/auth-manager/src/key/models/keyManager.ts b/apps/auth-manager/src/key/models/keyManager.ts index 6a133ebb..2c9a9406 100644 --- a/apps/auth-manager/src/key/models/keyManager.ts +++ b/apps/auth-manager/src/key/models/keyManager.ts @@ -22,14 +22,12 @@ export class KeyManager { public async getEnvKeys(environment: Environments): Promise { this.logger.info({ msg: 'fetching all specific environment keys', key: { environment } }); - // return this.keyRepository.find({ where: { environment } }); return this.drizzle.query.key.findMany({ where: { environment } }); } public async getKey(environment: Environments, version: number): Promise { this.logger.info({ msg: 'fetching key', key: { environment, version } }); - // const key = await this.keyRepository.findOne({ where: { environment, version } }); const key = await this.drizzle.query.key.findFirst({ where: { environment, version } }); if (key === undefined) { @@ -51,32 +49,7 @@ export class KeyManager { public async upsertKey(key: NewKey): Promise { this.logger.info({ msg: 'upserting key', key: { environment: key.environment, version: key.version } }); - // return this.keyRepository.manager.transaction(async (transactionManager) => { - // const transactionRepo = transactionManager.withRepository(this.keyRepository); - // const maxVersion = await transactionRepo.getMaxVersionWithLock(key.environment); - - // if (maxVersion === null) { - // if (key.version !== 1) { - // const msg = 'given key version is not 1, when no key already exists'; - // this.logger.debug({ msg, clientKeyVersion: key.version }); - // throw new KeyVersionMismatchError(msg); - // } - - // // insert - // return transactionRepo.save(key); - // } - - // if (maxVersion !== key.version) { - // const msg = 'version mismatch between database key and given key'; - // this.logger.debug({ msg, clientKeyVersion: key.version, dbKeyVersion: maxVersion }); - - // throw new KeyVersionMismatchError(msg); - // } - - // // update - // return transactionRepo.save({ ...key, version: maxVersion + 1 }); - // }); return this.drizzle.transaction(async (tx) => { const maxVersion = await this.keyRepository.getMaxVersionWithLock(key.environment, tx); diff --git a/apps/auth-manager/tests/integration/bundle/bundle.spec.mts b/apps/auth-manager/tests/integration/bundle/bundle.spec.mts index d2520bdf..ab2a6fe1 100644 --- a/apps/auth-manager/tests/integration/bundle/bundle.spec.mts +++ b/apps/auth-manager/tests/integration/bundle/bundle.spec.mts @@ -2,7 +2,6 @@ import { afterEach, describe, expect, it, vi, beforeAll } from 'vitest'; import { faker } from '@faker-js/faker'; import httpStatusCodes from 'http-status-codes'; -import type { DependencyContainer } from 'tsyringe'; import type { Drizzle, Environments, Bundle } from '@map-colonies/auth-core'; import { bundleTable, Environment } from '@map-colonies/auth-core'; import 'jest-openapi'; @@ -31,7 +30,6 @@ describe('bundle', function () { .values([getFakeBundle(), { ...getFakeBundle(), environment: Environment.PROD }, getFakeBundle()]) .returning() ).map((b) => ({ ...b, createdAt: b.createdAt.toISOString() })) as [ApiBundle, ApiBundle, ApiBundle]; - // bundles.forEach((b) => delete b.createdAt); }); describe('Happy Path', function () { diff --git a/apps/auth-manager/tests/integration/client/client.spec.mts b/apps/auth-manager/tests/integration/client/client.spec.mts index 6dc0cc98..dbb528e5 100644 --- a/apps/auth-manager/tests/integration/client/client.spec.mts +++ b/apps/auth-manager/tests/integration/client/client.spec.mts @@ -1,7 +1,6 @@ /// import { beforeAll, describe, expect, it, afterEach, vi } from 'vitest'; import httpStatusCodes from 'http-status-codes'; -import type { DependencyContainer } from 'tsyringe'; import { faker } from '@faker-js/faker'; import 'jest-openapi'; import type { Drizzle } from '@map-colonies/auth-core'; @@ -35,7 +34,6 @@ describe('client', function () { it('should return 200 status code and a list of clients', async function () { const clients = [getFakeClient(false), getFakeClient(false)]; - // const connection = depContainer.resolve(DataSource); await drizzle.insert(clientTable).values(clients).execute(); const res = await requestSender.getClients(); @@ -63,7 +61,6 @@ describe('client', function () { expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - console.log(res.body); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect((res.body as Exclude).items).toSatisfyAny((item) => item.name === name); }); @@ -130,7 +127,6 @@ describe('client', function () { }); it('should support pagination with different page numbers', async function () { - // Generated by Copilot const TOTAL_CLIENTS = 7; const PAGE_SIZE = 3; const SECOND_PAGE = 2; @@ -157,7 +153,6 @@ describe('client', function () { }); it('should handle last page with fewer items', async function () { - // Generated by Copilot const TOTAL_CLIENTS = 5; const PAGE_SIZE = 3; const LAST_PAGE = 2; @@ -242,7 +237,6 @@ describe('client', function () { }); it('should support sorting by createdAt in ascending order', async function () { - // Generated by Copilot const dates = [new Date('2023-01-01'), new Date('2023-03-01'), new Date('2023-02-01')]; const clients = dates.map((date, index) => ({ @@ -266,7 +260,6 @@ describe('client', function () { // Check if items are sorted by createdAt in ascending order const sortedItems = res.body.items; for (let i = 0; i < sortedItems.length - 1; i++) { - // Generated by Copilot const currentItem = sortedItems[i]; const nextItem = sortedItems[i + 1]; @@ -318,7 +311,6 @@ describe('client', function () { }); it('should return empty results for page beyond available data', async function () { - // Generated by Copilot const TOTAL_CLIENTS = 3; const PAGE_SIZE = 5; const BEYOND_AVAILABLE_PAGE = 2; @@ -328,8 +320,6 @@ describe('client', function () { name: `empty-test-client-${index}`, })); - // const connection = depContainer.resolve(DataSource); - // await connection.getRepository(Client).insert(clients.map((c) => ({ ...c }))); await drizzle.insert(clientTable).values(clients); const res = await requestSender.getClients({ @@ -361,8 +351,6 @@ describe('client', function () { it('should return 200 status code and the client', async function () { const client = getFakeClient(false); - // const connection = depContainer.resolve(DataSource); - // await connection.getRepository(Client).insert({ ...client }); await drizzle.insert(clientTable).values(client); const res = await requestSender.getClient({ pathParams: { clientName: client.name } }); @@ -377,8 +365,6 @@ describe('client', function () { it('should return 200 status code and the updated client', async function () { const client = getFakeClient(false); - // const connection = depContainer.resolve(DataSource); - // await connection.getRepository(Client).insert({ ...client }); await drizzle.insert(clientTable).values(client); const res = await requestSender.updateClient({ @@ -482,8 +468,6 @@ describe('client', function () { describe('POST /client', function () { it('should return 500 status code if db throws an error', async function () { - // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - // vi.spyOn(repo, 'insert').mockRejectedValue(new Error()); vi.spyOn(drizzle, 'insert').mockRejectedValue(new Error()); const res = await requestSender.createClient({ requestBody: getFakeClient(false) as unknown as ApiClient }); @@ -495,8 +479,6 @@ describe('client', function () { describe('GET /client/:clientName', function () { it('should return 500 status code if db throws an error', async function () { - // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - // vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); vi.spyOn(drizzle.query.client, 'findFirst').mockRejectedValue(new Error()); const res = await requestSender.getClient({ pathParams: { clientName: 'avi' } }); @@ -508,8 +490,6 @@ describe('client', function () { describe('PATCH /client/:clientName', function () { it('should return 500 status code if db throws an error', async function () { - // const repo = depContainer.resolve(SERVICES.CLIENT_REPOSITORY); - // vi.spyOn(repo, 'updateAndReturn').mockRejectedValue(new Error()); vi.spyOn(drizzle, 'update').mockRejectedValue(new Error()); const res = await requestSender.updateClient({ diff --git a/apps/auth-manager/tests/integration/connection/connection.spec.mts b/apps/auth-manager/tests/integration/connection/connection.spec.mts index 078e5c9c..911d2dbb 100644 --- a/apps/auth-manager/tests/integration/connection/connection.spec.mts +++ b/apps/auth-manager/tests/integration/connection/connection.spec.mts @@ -72,10 +72,10 @@ describe('connection', function () { it.each([ { name: 'avi', searchParam: 'avi', matchType: 'exact' }, - // { name: 'bobavi', searchParam: 'avi', matchType: 'suffix' }, - // { name: 'aviiiiii', searchParam: 'av', matchType: 'prefix' }, - // { name: 'blaviabla', searchParam: 'avi', matchType: 'middle' }, - // { name: 'avi', searchParam: 'AV', matchType: 'case-insensitive' }, + { name: 'bobavi', searchParam: 'avi', matchType: 'suffix' }, + { name: 'aviiiiii', searchParam: 'av', matchType: 'prefix' }, + { name: 'blaviabla', searchParam: 'avi', matchType: 'middle' }, + { name: 'avi', searchParam: 'AV', matchType: 'case-insensitive' }, ])('should find the connection of $name with search string $searchParam with match type $matchType', async function ({ name, searchParam }) { const client = { ...getFakeClient(false, { name }) }; const connection = getFakeConnection(false, { name: client.name }); @@ -105,7 +105,7 @@ describe('connection', function () { const res = await requestSender.getConnections({ queryParams: { - name: 'avi', + name: 'rofl', }, }); @@ -312,7 +312,8 @@ describe('connection', function () { expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toStrictEqual({ ...connections[2], createdAt: connections[2]!.createdAt?.toISOString() }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect(res.body).toStrictEqual({ ...connections[2], createdAt: expect.any(String) }); }); }); @@ -347,10 +348,9 @@ describe('connection', function () { describe('Bad Path', function () { describe('POST /connection', function () { - it.only('should return 400 if the request body is incorrect', async function () { + it('should return 400 if the request body is incorrect', async function () { const connection = getFakeConnection(false, { name: clients[0]!.name, domains: [] }); const res = await requestSender.upsertConnection({ requestBody: connection }); - console.log(res.body); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); expect(res).toSatisfyApiSpec(); @@ -369,7 +369,6 @@ describe('connection', function () { const connection = getFakeConnection(false, { name: clients[0]!.name }); connection.token = ''; const res = await requestSender.upsertConnection({ requestBody: connection }); - console.log(res.body); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); expect(res).toSatisfyApiSpec(); @@ -518,7 +517,6 @@ describe('connection', function () { vi.spyOn(keyRepo, 'getLatestKeys').mockRejectedValue(new Error()); const res = await requestSender.upsertConnection({ requestBody: connection }); - console.log(res.body); expect(res).toHaveProperty('status', httpStatusCodes.INTERNAL_SERVER_ERROR); expect(res).toSatisfyApiSpec(); @@ -527,10 +525,6 @@ describe('connection', function () { describe('GET /client/:clientName/connection', function () { it('should return 500 status code if db throws an error', async function () { - // const repo = depContainer.resolve(ConnectionRepository); - // const qbMock = { - // getMany: vi.fn().mockRejectedValue(new Error('DB Error')), - // }; vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getClientConnections({ pathParams: { clientName: 'avi' } }); @@ -542,10 +536,6 @@ describe('connection', function () { describe('GET /client/:clientName/connection/:environment', function () { it('should return 500 status code if db throws an error', async function () { - // const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); - // const qbMock = { - // getMany: vi.fn().mockRejectedValue(new Error('DB Error')), - // }; vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getClientEnvironmentConnections({ pathParams: { clientName: 'avi', environment: Environment.NP } }); @@ -562,8 +552,6 @@ describe('connection', function () { connection.environment = Environment.NP; await requestSender.upsertConnection({ requestBody: connection }); - // const repo = depContainer.resolve(SERVICES.CONNECTION_REPOSITORY); - // vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); vi.spyOn(drizzle.query.connection, 'findFirst').mockRejectedValue(new Error()); const res = await requestSender.getClientVersionedConnection({ diff --git a/apps/auth-manager/tests/integration/docs/docs.spec.ts b/apps/auth-manager/tests/integration/docs/docs.spec.ts index fb2ee949..94cc1d20 100644 --- a/apps/auth-manager/tests/integration/docs/docs.spec.ts +++ b/apps/auth-manager/tests/integration/docs/docs.spec.ts @@ -3,8 +3,7 @@ import { jsLogger } from '@map-colonies/js-logger'; import { trace } from '@opentelemetry/api'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; -import { DataSource } from 'typeorm'; - +import { Pool } from 'pg'; import { getApp } from '../../../src/app'; import { initConfig } from '../../../src/common/config'; import { SERVICES } from '../../../src/common/constants'; @@ -31,7 +30,7 @@ describe('docs', function () { }); afterEach(async function () { - await depContainer.resolve(DataSource).destroy(); + await depContainer.resolve(Pool).end(); }); describe('Happy Path', function () { diff --git a/apps/auth-manager/tests/integration/domain/domain.spec.mts b/apps/auth-manager/tests/integration/domain/domain.spec.mts index 7c32e688..d4ccaafd 100644 --- a/apps/auth-manager/tests/integration/domain/domain.spec.mts +++ b/apps/auth-manager/tests/integration/domain/domain.spec.mts @@ -1,8 +1,7 @@ -import { beforeEach, describe, expect, it, beforeAll, afterAll, vi } from 'vitest'; +import { beforeEach, describe, expect, it, beforeAll, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; import { trace } from '@opentelemetry/api'; import httpStatusCodes from 'http-status-codes'; -import type { DependencyContainer } from 'tsyringe'; import { faker } from '@faker-js/faker'; import 'jest-openapi'; import { Pool } from 'pg'; @@ -13,7 +12,6 @@ import { createRequestSender, expectResponseStatusFactory } from '@map-colonies/ import type { paths, operations } from 'auth-openapi'; import { getApp } from '@src/app.js'; import { SERVICES } from '@src/common/constants.js'; -import { initConfig } from '@src/common/config.js'; import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; import { initEnvironment } from '../setup.js'; @@ -32,7 +30,6 @@ describe('domain', function () { describe('Happy Path', function () { describe('GET /domain', function () { it('should return 200 status code and a list of domains', async function () { - // const drizzle = depContainer.resolve(SERVICES.DRIZZLE); await drizzle.insert(domainTable).values([{ name: 'avi' }, { name: 'iva' }]); const res = await requestSender.getDomains(); diff --git a/apps/auth-manager/tests/integration/key/key.spec.mts b/apps/auth-manager/tests/integration/key/key.spec.mts index 5e053ab6..6a795f5d 100644 --- a/apps/auth-manager/tests/integration/key/key.spec.mts +++ b/apps/auth-manager/tests/integration/key/key.spec.mts @@ -1,60 +1,41 @@ /// -import { beforeEach, describe, expect, it, vi, beforeAll, afterEach } from 'vitest'; -import { jsLogger } from '@map-colonies/js-logger'; -import { trace } from '@opentelemetry/api'; +import { describe, expect, it, vi, beforeAll, afterEach } from 'vitest'; import httpStatusCodes from 'http-status-codes'; -import type { DependencyContainer } from 'tsyringe'; import 'jest-openapi'; -import { DataSource } from 'typeorm'; import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import type { IKey, Environments } from '@map-colonies/auth-core'; -import { Key, Environment } from '@map-colonies/auth-core'; +import { eq } from 'drizzle-orm'; +import type { Drizzle, Environments, Key } from '@map-colonies/auth-core'; +import { Environment, keyTable } from '@map-colonies/auth-core'; import { getMockKeys } from 'test-utils'; +import type { DependencyContainer } from 'tsyringe'; import type { paths, operations, components } from 'auth-openapi'; -import { getApp } from '@src/app.js'; -import { SERVICES } from '@src/common/constants.js'; -import type { KeyRepository } from '@src/key/DAL/keyRepository.js'; -import { initConfig } from '@src/common/config.js'; -import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; +import { KeyRepository } from '@src/key/DAL/keyRepository.js'; +import { initEnvironment } from '../setup.js'; describe('key', function () { let requestSender: RequestSender; + let drizzle: Drizzle; let depContainer: DependencyContainer; beforeAll(async function () { - await initConfig(true); - }); - - beforeEach(async function () { - const [app, container] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender(OPENAPI_PATH, app); - depContainer = container; - }); - - afterEach(async function () { - await depContainer.resolve(DataSource).destroy(); + const env = await initEnvironment(); + requestSender = env.requestSender; + drizzle = env.drizzle; + depContainer = env.container; }); describe('Happy Path', function () { describe('GET /key', function () { it('should return 200 status code and all the latest keys', async function () { const [privateKey, publicKey] = getMockKeys(); - const keys: IKey[] = [ + const keys: Key[] = [ { environment: Environment.NP, version: 1, privateKey, publicKey }, { environment: Environment.NP, version: 2, privateKey, publicKey }, + { environment: Environment.STAGE, version: 2, privateKey, publicKey }, { environment: Environment.STAGE, version: 1, privateKey, publicKey }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).save(keys); - + await drizzle.insert(keyTable).values(keys).onConflictDoNothing(); const res = await requestSender.getLastestKeys(); expect(res).toHaveProperty('status', httpStatusCodes.OK); @@ -68,36 +49,36 @@ describe('key', function () { it('should return 201 status code and the created key', async function () { const [privateKey, publicKey] = getMockKeys(); - const res = await requestSender.upsertKey({ requestBody: { version: 1, environment: Environment.PRODUCTION, privateKey, publicKey } }); + const res = await requestSender.upsertKey({ requestBody: { version: 1, environment: Environment.PROD, privateKey, publicKey } }); expect(res).toHaveProperty('status', httpStatusCodes.CREATED); expect(res).toSatisfyApiSpec(); - expect(res.body).toMatchObject({ version: 1, environment: Environment.PRODUCTION, privateKey, publicKey }); + expect(res.body).toMatchObject({ version: 1, environment: Environment.PROD, privateKey, publicKey }); }); it('should return 200 status code and the updated key', async function () { const [privateKey, publicKey] = getMockKeys(); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).save({ version: 1, environment: Environment.PRODUCTION, privateKey, publicKey }); + await drizzle.insert(keyTable).values({ version: 1, environment: Environment.PROD, privateKey, publicKey }).onConflictDoNothing(); - const res = await requestSender.upsertKey({ requestBody: { version: 1, environment: Environment.PRODUCTION, privateKey, publicKey } }); + const res = await requestSender.upsertKey({ requestBody: { version: 1, environment: Environment.PROD, privateKey, publicKey } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); - expect(res.body).toMatchObject({ version: 2, environment: Environment.PRODUCTION, privateKey, publicKey }); + expect(res.body).toMatchObject({ version: 2, environment: Environment.PROD, privateKey, publicKey }); }); }); describe('GET /key/:environment', function () { it('should return 200 status code all the keys in the specific environment', async function () { const [privateKey, publicKey] = getMockKeys(); - const keys: IKey[] = [ + const keys: Key[] = [ { environment: Environment.STAGE, version: 2, privateKey, publicKey }, { environment: Environment.STAGE, version: 1, privateKey, publicKey }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).save(keys); + // @ts-expect-error - eq throws an error for some weird reason + await drizzle.delete(keyTable).where(eq(keyTable.environment, Environment.STAGE)); + await drizzle.insert(keyTable).values(keys).onConflictDoNothing(); const res = await requestSender.getKeys({ pathParams: { environment: Environment.STAGE } }); @@ -111,8 +92,7 @@ describe('key', function () { it('should return 200 status code and the requested key', async function () { const [privateKey, publicKey] = getMockKeys(); const key = { environment: Environment.NP, version: 3, privateKey, publicKey }; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).save(key); + await drizzle.insert(keyTable).values(key).onConflictDoNothing(); const res = await requestSender.getSpecificKey({ pathParams: { environment: Environment.NP, version: 3 } }); @@ -125,14 +105,13 @@ describe('key', function () { describe('GET /key/:environment/latest', () => { it('should return 200 status code and the latest key when multiple versions exist', async function () { const [privateKey, publicKey] = getMockKeys(); - const keys: IKey[] = [ + const keys: Key[] = [ { environment: Environment.STAGE, version: 1, privateKey, publicKey }, { environment: Environment.STAGE, version: 2, privateKey, publicKey }, { environment: Environment.STAGE, version: 3, privateKey, publicKey }, ]; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).save(keys); + await drizzle.insert(keyTable).values(keys).onConflictDoNothing(); const expectedKey = keys.find((k) => k.version === 3); @@ -145,13 +124,13 @@ describe('key', function () { it('should return 200 status code and the only key when there is only one version', async function () { const [privateKey, publicKey] = getMockKeys(); - const key: IKey = { environment: Environment.PRODUCTION, version: 1, privateKey, publicKey }; + const key: Key = { environment: Environment.PROD, version: 1, privateKey, publicKey }; - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).delete({ environment: Environment.PRODUCTION }); // Ensure no previous keys exist - await connection.getRepository(Key).save(key); + // @ts-expect-error - eq throws an error for some weird reason + await drizzle.delete(keyTable).where(eq(keyTable.environment, Environment.PROD)); // Ensure no previous keys exist + await drizzle.insert(keyTable).values(key); - const res = await requestSender.getLatestKey({ pathParams: { environment: Environment.PRODUCTION } }); + const res = await requestSender.getLatestKey({ pathParams: { environment: Environment.PROD } }); expect(res).toHaveProperty('status', httpStatusCodes.OK); expect(res).toSatisfyApiSpec(); @@ -185,8 +164,8 @@ describe('key', function () { it("should return 409 if the no key exists and request version isn't 1", async function () { const [privateKey, publicKey] = getMockKeys(); - const connection = depContainer.resolve(DataSource); - await connection.getRepository(Key).delete({ environment: Environment.NP }); + // @ts-expect-error - eq throws an error for some weird reason + await drizzle.delete(keyTable).where(eq(keyTable.environment, Environment.NP)); const res = await requestSender.upsertKey({ requestBody: { environment: Environment.NP, version: 2, privateKey, publicKey } }); @@ -206,14 +185,14 @@ describe('key', function () { describe('GET /key/:environment/:version', function () { it('should return 400 if version value is not valid', async function () { - const res = await requestSender.getSpecificKey({ pathParams: { environment: Environment.PRODUCTION, version: -1 } }); + const res = await requestSender.getSpecificKey({ pathParams: { environment: Environment.PROD, version: -1 } }); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); expect(res).toSatisfyApiSpec(); }); it("should return 404 if the key doesn't exist", async function () { - const res = await requestSender.getSpecificKey({ pathParams: { environment: Environment.PRODUCTION, version: 999 } }); + const res = await requestSender.getSpecificKey({ pathParams: { environment: Environment.PROD, version: 999 } }); expect(res).toHaveProperty('status', httpStatusCodes.NOT_FOUND); expect(res).toSatisfyApiSpec(); @@ -229,9 +208,10 @@ describe('key', function () { }); it('should return 404 if no key exists for the given environment', async function () { - await depContainer.resolve(DataSource).getRepository(Key).delete({ environment: Environment.PRODUCTION }); + // @ts-expect-error - eq throws an error for some weird reason + await drizzle.delete(keyTable).where(eq(keyTable.environment, Environment.PROD)); - const res = await requestSender.getLatestKey({ pathParams: { environment: Environment.PRODUCTION } }); + const res = await requestSender.getLatestKey({ pathParams: { environment: Environment.PROD } }); expect(res).toHaveProperty('status', httpStatusCodes.NOT_FOUND); expect(res).toSatisfyApiSpec(); @@ -246,7 +226,7 @@ describe('key', function () { describe('GET /key', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.KEY_REPOSITORY); + const repo = depContainer.resolve(KeyRepository); vi.spyOn(repo, 'getLatestKeys').mockRejectedValue(new Error()); const res = await requestSender.getLastestKeys(); @@ -258,7 +238,7 @@ describe('key', function () { describe('POST /key', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.KEY_REPOSITORY); + const repo = depContainer.resolve(KeyRepository); vi.spyOn(repo, 'getMaxVersionWithLock').mockRejectedValue(new Error()); const [privateKey, publicKey] = getMockKeys(); @@ -270,8 +250,7 @@ describe('key', function () { describe('GET /key/:environment', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.KEY_REPOSITORY); - vi.spyOn(repo, 'find').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.key, 'findMany').mockRejectedValue(new Error()); const res = await requestSender.getKeys({ pathParams: { environment: Environment.NP } }); @@ -282,8 +261,7 @@ describe('key', function () { describe('GET /key/:environment/:version', function () { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.KEY_REPOSITORY); - vi.spyOn(repo, 'findOne').mockRejectedValue(new Error()); + vi.spyOn(drizzle.query.key, 'findFirst').mockRejectedValue(new Error()); const res = await requestSender.getSpecificKey({ pathParams: { environment: Environment.NP, version: 1 } }); @@ -294,7 +272,7 @@ describe('key', function () { describe('GET /key/:environment/latest', () => { it('should return 500 status code if db throws an error', async function () { - const repo = depContainer.resolve(SERVICES.KEY_REPOSITORY); + const repo = depContainer.resolve(KeyRepository); vi.spyOn(repo, 'getMaxVersion').mockRejectedValue(new Error()); const res = await requestSender.getLatestKey({ pathParams: { environment: Environment.NP } }); diff --git a/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts b/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts index 794d78af..586b99dc 100644 --- a/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts +++ b/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; import { getFakeAsset } from 'test-utils'; +import type { Drizzle } from '@map-colonies/auth-core'; import { AssetManager } from '@src/asset/models/assetManager.js'; import { AssetVersionMismatchError } from '@src/asset/models/errors.js'; import type { AssetRepository } from '@src/asset/DAL/assetRepository.js'; @@ -8,69 +9,67 @@ import type { AssetRepository } from '@src/asset/DAL/assetRepository.js'; const logger = await jsLogger({ enabled: false }); describe('AssetManager', () => { - let assetManager: AssetManager; - const mockedRepository = { - findBy: vi.fn(), - findOne: vi.fn(), - transaction: vi.fn(), - }; - - beforeEach(function () { - assetManager = new AssetManager(logger, mockedRepository as unknown as AssetRepository); - vi.resetAllMocks(); - }); - describe('#upsertAsset', () => { let manager: AssetManager; - const transactionRepo = { + const mockReturning = vi.fn(); + const mockWhere = vi.fn(); + const mockSet = vi.fn(); + const mockTx = { + insert: vi.fn(), + update: vi.fn(), + }; + const assetRepository = { getMaxVersionWithLock: vi.fn(), - save: vi.fn(), + getMaxVersion: vi.fn(), + }; + const drizzle = { + transaction: vi.fn(), }; beforeEach(function () { vi.resetAllMocks(); - const repo = { manager: { transaction: vi.fn() } }; - repo.manager.transaction.mockImplementation(async (fn: (a: unknown) => Promise) => { - return fn({ withRepository: vi.fn().mockReturnValue(transactionRepo) }); - }); - - manager = new AssetManager(logger, repo as unknown as AssetRepository); + mockReturning.mockResolvedValue([]); + mockWhere.mockReturnValue({ returning: mockReturning }); + mockSet.mockReturnValue({ where: mockWhere }); + mockTx.update.mockReturnValue({ set: mockSet }); + drizzle.transaction.mockImplementation(async (fn: (tx: unknown) => Promise) => fn(mockTx)); + manager = new AssetManager(logger, assetRepository as unknown as AssetRepository, drizzle as unknown as Drizzle); }); it('should update the asset,return it, and advance the version by 1 if it exist in the database and the version matches', async () => { const asset = getFakeAsset(); asset.version = 2; - transactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - transactionRepo.save.mockResolvedValue(asset); + assetRepository.getMaxVersionWithLock.mockResolvedValue(1); + mockReturning.mockResolvedValue([asset]); const assetPromise = manager.upsertAsset({ ...asset, version: 1 }); await expect(assetPromise).resolves.toStrictEqual(asset); - expect(transactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(transactionRepo.save).toHaveBeenCalledTimes(1); - expect(transactionRepo.save).toHaveBeenCalledWith(asset); + expect(assetRepository.getMaxVersionWithLock).toHaveBeenCalledTimes(1); + expect(mockTx.update).toHaveBeenCalledTimes(1); + expect(mockSet).toHaveBeenCalledWith(asset); }); it("should throw an error if a asset doesn't exist and the version supplied is not 1", async () => { const asset = getFakeAsset(); - transactionRepo.getMaxVersionWithLock.mockResolvedValue(null); + assetRepository.getMaxVersionWithLock.mockResolvedValue(null); const assetPromise = manager.upsertAsset({ ...asset, version: 2 }); await expect(assetPromise).rejects.toThrow(AssetVersionMismatchError); - expect(transactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(transactionRepo.save).not.toHaveBeenCalled(); + expect(assetRepository.getMaxVersionWithLock).toHaveBeenCalledTimes(1); + expect(mockTx.insert).not.toHaveBeenCalled(); }); it("should throw an error if a asset exist but the supplied version doesn't match database version", async () => { const asset = getFakeAsset(); - transactionRepo.getMaxVersionWithLock.mockResolvedValue(1); + assetRepository.getMaxVersionWithLock.mockResolvedValue(1); const assetPromise = manager.upsertAsset({ ...asset, version: 2 }); await expect(assetPromise).rejects.toThrow(AssetVersionMismatchError); - expect(transactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(transactionRepo.save).not.toHaveBeenCalled(); + expect(assetRepository.getMaxVersionWithLock).toHaveBeenCalledTimes(1); + expect(mockTx.update).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts index 1ba24400..313f54aa 100644 --- a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts +++ b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts @@ -1,40 +1,43 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; import { DatabaseError } from 'pg'; -import { QueryFailedError } from 'typeorm'; import { getFakeClient } from 'test-utils'; -import type { ClientRepository } from '@src/client/DAL/clientRepository.js'; +import type { Drizzle } from '@map-colonies/auth-core'; import { ClientManager } from '@src/client/models/clientManager.js'; -import { ClientAlreadyExistsError, ClientNotFoundError } from '@src/client/models/errors.js'; -import { PgErrorCodes } from '@src/common/db/constants.js'; +import { ClientAlreadyExistsError } from '@src/client/models/errors.js'; +import { pgErrorCodes } from '@src/common/db/constants.js'; const logger = await jsLogger({ enabled: false }); describe('ClientManager', () => { let clientManager: ClientManager; - const mockedRepository = { - findAndCount: vi.fn(), + const mockReturning = vi.fn(); + const mockValues = vi.fn(); + const drizzle = { insert: vi.fn(), - findOne: vi.fn(), - updateAndReturn: vi.fn(), }; beforeEach(function () { - clientManager = new ClientManager(logger, mockedRepository as unknown as ClientRepository); vi.resetAllMocks(); + mockValues.mockReturnValue({ returning: mockReturning }); + drizzle.insert.mockReturnValue({ values: mockValues }); + clientManager = new ClientManager(logger, drizzle as unknown as Drizzle); }); describe('#createClient', () => { it('should throw AlreadyExistsError if the client is already in', async function () { const client = getFakeClient(false); const dbError = new DatabaseError('avi', 5, 'error'); - dbError.code = PgErrorCodes.UNIQUE_VIOLATION; - mockedRepository.insert.mockRejectedValue(new QueryFailedError('avi', undefined, dbError)); + dbError.code = pgErrorCodes.UNIQUE_VIOLATION; + const drizzleError = new Error('query error'); + drizzleError.name = 'DrizzleQueryError'; + (drizzleError as Error & { cause: unknown }).cause = dbError; + mockReturning.mockRejectedValue(drizzleError); const domainPromise = clientManager.createClient(client); await expect(domainPromise).rejects.toThrow(ClientAlreadyExistsError); - expect(mockedRepository.insert).toHaveBeenCalled(); + expect(drizzle.insert).toHaveBeenCalled(); }); }); }); diff --git a/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts b/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts index de1390ba..bf88d6a9 100644 --- a/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts +++ b/apps/auth-manager/tests/unit/connection/models/connectionManager.spec.mts @@ -1,108 +1,78 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; -import { Environment } from '@map-colonies/auth-core'; -import { getRealKeys, getFakeConnection } from 'test-utils'; +import { getFakeConnection } from 'test-utils'; +import type { Drizzle } from '@map-colonies/auth-core'; import { ConnectionManager } from '@src/connection/models/connectionManager.js'; -import { ConnectionNotFoundError, ConnectionVersionMismatchError } from '@src/connection/models/errors.js'; import type { ConnectionRepository } from '@src/connection/DAL/connectionRepository.js'; import type { DomainRepository } from '@src/domain/DAL/domainRepository.js'; -import { ClientNotFoundError } from '@src/client/models/errors.js'; -import { DomainNotFoundError } from '@src/domain/models/errors.js'; import type { KeyRepository } from '@src/key/DAL/keyRepository.js'; -import { KeyNotFoundError } from '@src/key/models/errors.js'; const logger = await jsLogger({ enabled: false }); describe('ConnectionManager', () => { - let connectionManager: ConnectionManager; - const mockedConnectionRepository = { - findAndCount: vi.fn(), - findOne: vi.fn(), - transaction: vi.fn(), - createQueryBuilder: vi.fn(), - }; - const mockedDomainRepository = {}; - const mockedKeysRepository = {}; - - beforeEach(function () { - connectionManager = new ConnectionManager( - logger, - mockedConnectionRepository as unknown as ConnectionRepository, - mockedDomainRepository as DomainRepository, - mockedKeysRepository as KeyRepository - ); - vi.resetAllMocks(); - }); - describe('#upsertConnection', () => { let manager: ConnectionManager; - const connectionTransactionRepo = { + const mockReturning = vi.fn(); + const mockValues = vi.fn(); + const mockTx = { + insert: vi.fn(), + query: { + client: { + findFirst: vi.fn(), + }, + }, + }; + const connectionRepository = { getMaxVersionWithLock: vi.fn(), - save: vi.fn(), + getMaxVersion: vi.fn(), }; - const domainTransactionRepo = { + const domainRepository = { checkInputForNonExistingDomains: vi.fn(), }; - const clientTransactionRepo = { - findOneBy: vi.fn(), - }; - const keyTransactionRepo = { + const keyRepository = { getLatestKeys: vi.fn(), }; + const drizzle = { + transaction: vi.fn(), + }; beforeEach(function () { vi.resetAllMocks(); - clientTransactionRepo.findOneBy.mockResolvedValue({}); - domainTransactionRepo.checkInputForNonExistingDomains.mockResolvedValue([]); - const connectionRepo = { manager: { transaction: vi.fn() } }; - const domainRepo = {}; - const keysRepo = {}; - connectionRepo.manager.transaction.mockImplementation(async (fn: (a: unknown) => Promise) => { - return fn({ - withRepository: vi.fn().mockImplementation((callValue) => { - switch (callValue) { - case connectionRepo: - return connectionTransactionRepo; - case domainRepo: - return domainTransactionRepo; - case keysRepo: - return keyTransactionRepo; - default: - throw new Error('unknown repo'); - } - }), - getRepository: vi.fn().mockReturnValue(clientTransactionRepo), - }); - }); + mockTx.query.client.findFirst.mockResolvedValue({}); + domainRepository.checkInputForNonExistingDomains.mockResolvedValue([]); + mockValues.mockReturnValue({ returning: mockReturning }); + mockTx.insert.mockReturnValue({ values: mockValues }); + drizzle.transaction.mockImplementation(async (fn: (tx: unknown) => Promise) => fn(mockTx)); manager = new ConnectionManager( logger, - connectionRepo as unknown as ConnectionRepository, - domainRepo as DomainRepository, - keysRepo as KeyRepository + connectionRepository as unknown as ConnectionRepository, + domainRepository as unknown as DomainRepository, + keyRepository as unknown as KeyRepository, + drizzle as unknown as Drizzle ); }); it('should update the connection,return it, and advance the version by 1 if it exist in the database and the version matches', async () => { const connection = getFakeConnection(); connection.version = 2; - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - connectionTransactionRepo.save.mockResolvedValue(connection); + connectionRepository.getMaxVersionWithLock.mockResolvedValue(1); + mockReturning.mockResolvedValue([connection]); const connectionPromise = manager.upsertConnection({ ...connection, version: 1 }); await expect(connectionPromise).resolves.toStrictEqual(connection); - expect(connectionTransactionRepo.getMaxVersionWithLock).toHaveBeenCalledTimes(1); - expect(connectionTransactionRepo.save).toHaveBeenCalledTimes(1); - expect(connectionTransactionRepo.save).toHaveBeenCalledWith(connection); + expect(connectionRepository.getMaxVersionWithLock).toHaveBeenCalledTimes(1); + expect(mockTx.insert).toHaveBeenCalledTimes(1); + expect(mockValues).toHaveBeenCalledWith(connection); }); it('should return the connection with empty token if the token generation failed and ignoreTokenErrors is true', async () => { const connection = getFakeConnection(); connection.token = ''; - connectionTransactionRepo.getMaxVersionWithLock.mockResolvedValue(1); - connectionTransactionRepo.save.mockResolvedValue(connection); - keyTransactionRepo.getLatestKeys = vi.fn().mockResolvedValue([{ environment: connection.environment, privateKey: 'avi' }]); + connectionRepository.getMaxVersionWithLock.mockResolvedValue(1); + mockReturning.mockResolvedValue([connection]); + keyRepository.getLatestKeys = vi.fn().mockResolvedValue([{ environment: connection.environment, privateKey: 'avi' }]); const connectionRes = await manager.upsertConnection({ ...connection, version: 1 }, true); diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index 29945626..8e5cd986 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -13,7 +13,10 @@ }, "exports": { ".": { - "import": "./dist/index.js", + "import": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, "require": "./dist/index.js" } }, @@ -36,7 +39,6 @@ }, "dependencies": { "pg": "catalog:", - "drizzle-orm": "catalog:", "@map-colonies/schemas": "catalog:" }, "devDependencies": { @@ -50,6 +52,7 @@ "drizzle-kit": "catalog:" }, "peerDependencies": { - "@map-colonies/js-logger": "catalog:" + "@map-colonies/js-logger": "catalog:", + "drizzle-orm": "catalog:" } } diff --git a/packages/auth-openapi/openapi3.yaml b/packages/auth-openapi/openapi3.yaml index 6c47e8fc..9e3822c7 100644 --- a/packages/auth-openapi/openapi3.yaml +++ b/packages/auth-openapi/openapi3.yaml @@ -1018,7 +1018,7 @@ components: uniqueItems: true items: type: string - minLength: 1 + minItems: 1 createdAt: type: string format: date-time From df759ba324c22d467e6c0b61c87dd504248a646c Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Mon, 25 May 2026 17:17:54 +0300 Subject: [PATCH 08/14] chore(auth-bundler): done --- apps/auth-cron/package.json | 6 +- apps/auth-cron/src/index.ts | 14 +- apps/auth-cron/src/job.ts | 12 +- apps/auth-cron/src/util.ts | 1 - apps/auth-cron/tests/job.spec.mts | 35 ++-- apps/auth-cron/tests/s3.spec.mts | 4 +- packages/auth-bundler/dataSource.ts | 10 - packages/auth-bundler/package.json | 5 +- packages/auth-bundler/src/bundler.ts | 6 +- packages/auth-bundler/src/db.ts | 197 +++++++++++------- packages/auth-bundler/src/errors.ts | 12 +- packages/auth-bundler/src/util.ts | 10 +- packages/auth-bundler/tests/bundler.spec.mts | 4 +- .../tests/{db.spec.ts => db.spec.mts} | 114 ++++++---- packages/auth-bundler/tests/util.spec.ts | 4 +- packages/auth-bundler/tests/utils/asset.ts | 17 -- packages/auth-bundler/tests/utils/bundle.ts | 14 +- .../auth-bundler/tests/utils/connection.ts | 19 -- packages/auth-bundler/tests/utils/key.ts | 26 --- packages/test-utils/src/fakers.ts | 7 +- pnpm-lock.yaml | 167 +-------------- 21 files changed, 273 insertions(+), 411 deletions(-) delete mode 100644 packages/auth-bundler/dataSource.ts rename packages/auth-bundler/tests/{db.spec.ts => db.spec.mts} (50%) delete mode 100644 packages/auth-bundler/tests/utils/asset.ts delete mode 100644 packages/auth-bundler/tests/utils/connection.ts delete mode 100644 packages/auth-bundler/tests/utils/key.ts diff --git a/apps/auth-cron/package.json b/apps/auth-cron/package.json index 5d7e62ec..101853a1 100644 --- a/apps/auth-cron/package.json +++ b/apps/auth-cron/package.json @@ -15,9 +15,6 @@ "scripts": { "lint": "eslint .", "lint:fix": "eslint --fix .", - "migration:run": "npm run typeorm migration:run -- ", - "migration:revert": "npm run typeorm migration:revert -- ", - "typeorm": "node ../../node_modules/typeorm/cli.js -d ./dataSource.mjs", "test": "vitest run", "test:watch": "vitest watch", "test:ui": "vitest --ui", @@ -49,7 +46,7 @@ "http-status-codes": "^2.2.0", "pg": "catalog:", "prom-client": "catalog:", - "typeorm": "catalog:" + "drizzle-orm": "catalog:" }, "devDependencies": { "@types/node": "catalog:", @@ -57,6 +54,7 @@ "jest-extended": "catalog:", "test-utils": "workspace:^", "ts-node": "^10.9.1", + "@types/pg": "catalog:", "typescript": "catalog:", "@types/lodash": "catalog:", "vitest": "catalog:", diff --git a/apps/auth-cron/src/index.ts b/apps/auth-cron/src/index.ts index 82de5160..85739695 100644 --- a/apps/auth-cron/src/index.ts +++ b/apps/auth-cron/src/index.ts @@ -3,13 +3,13 @@ import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { env } from 'node:process'; import { createServer } from 'node:http'; +import type { Pool } from 'pg'; import express from 'express'; import { createTerminus } from '@godaddy/terminus'; import type { CatchCallbackFn } from 'croner'; import { Cron } from 'croner'; -import type { DataSource, Repository } from 'typeorm'; import type { Environments } from '@map-colonies/auth-core'; -import { Bundle, initConnection } from '@map-colonies/auth-core'; +import { initConnection } from '@map-colonies/auth-core'; import type { commonDbFullV1Type } from '@map-colonies/schemas'; import { BundleDatabase } from '@map-colonies/auth-bundler'; import { collectMetricsExpressMiddleware } from '@map-colonies/prometheus'; @@ -22,10 +22,10 @@ import { metricsRegistry } from './telemetry/metrics'; // eslint-disable-next-line @typescript-eslint/no-magic-numbers const SERVER_PORT = env['SERVER_PORT'] ?? 8080; -async function initDb(dbConfig: commonDbFullV1Type): Promise<[DataSource, BundleDatabase, Repository]> { +async function initDb(dbConfig: commonDbFullV1Type): Promise<[Pool, BundleDatabase]> { logger.debug('initializing database connection'); - const dataSource = await initConnection(dbConfig); - return [dataSource, new BundleDatabase(dataSource), dataSource.getRepository(Bundle)]; + const pool = await initConnection(dbConfig); + return [pool, new BundleDatabase(pool)]; } const errorHandler: CatchCallbackFn = (err, job) => { @@ -36,13 +36,13 @@ const main = async (): Promise => { const config = getConfig(); const cronConfig = config.get('cron'); const dbConfig = config.get('db'); - const [dataSource, bundleDatabase, bundleRepository] = await initDb(dbConfig); + const [dataSource, bundleDatabase] = await initDb(dbConfig); Object.entries(cronConfig).map(([env, value]) => { logger.info({ msg: 'initializing new update bundle job', bundleEnv: env }); const workdir = mkdtempSync(path.join(tmpdir(), `authbundler-${env}-`)); - const job = getJob(bundleRepository, bundleDatabase, env as Environments, workdir); + const job = getJob(bundleDatabase, env as Environments, workdir); return Cron(value.pattern, { unref: false, protect: true, catch: errorHandler, name: env }, async () => { logger.info({ msg: 'running new update job', bundleEnv: env }); diff --git a/apps/auth-cron/src/job.ts b/apps/auth-cron/src/job.ts index a857704f..7834ee95 100644 --- a/apps/auth-cron/src/job.ts +++ b/apps/auth-cron/src/job.ts @@ -1,21 +1,15 @@ import path from 'node:path'; import type { BundleDatabase } from '@map-colonies/auth-bundler'; import { createBundle, getVersionCommand } from '@map-colonies/auth-bundler'; -import type { Bundle, Environments } from '@map-colonies/auth-core'; -import type { Repository } from 'typeorm'; +import type { Environments } from '@map-colonies/auth-core'; import { getS3Client } from './s3'; import { compareVersionsToBundle } from './util'; import { logger } from './telemetry/logger'; -export function getJob( - bundleRepository: Repository, - bundleDatabase: BundleDatabase, - environment: Environments, - workdir: string -): () => Promise { +export function getJob(bundleDatabase: BundleDatabase, environment: Environments, workdir: string): () => Promise { return async () => { logger.debug({ msg: 'fetching bundle information from the database', bundleEnv: environment }); - const latestBundle = await bundleRepository.findOne({ where: { environment }, order: { id: 'DESC' } }); + const latestBundle = await bundleDatabase.getLatestBundleByEnv(environment); const latestVersions = await bundleDatabase.getLatestVersions(environment); const currentOpaVersion = await getVersionCommand(); diff --git a/apps/auth-cron/src/util.ts b/apps/auth-cron/src/util.ts index 7802dee4..7cdd579a 100644 --- a/apps/auth-cron/src/util.ts +++ b/apps/auth-cron/src/util.ts @@ -17,7 +17,6 @@ export function compareVersionsToBundle(bundle: Bundle, versions: BundleContentV // Added the check because keyVersion can be null in the database // and we want a new bundle to be created in that case // the bigger fix should be to not allow null keyVersion in the database - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition return bundle.environment === versions.environment && bundle.keyVersion !== null && bundle.keyVersion === versions.keyVersion; } catch { return false; diff --git a/apps/auth-cron/tests/job.spec.mts b/apps/auth-cron/tests/job.spec.mts index 194f8a8f..95d4cdd0 100644 --- a/apps/auth-cron/tests/job.spec.mts +++ b/apps/auth-cron/tests/job.spec.mts @@ -26,10 +26,10 @@ vi.mock('../src/telemetry/logger', async () => { describe('job.ts', function () { describe('#getJob', function () { - const bundleRepoMock = vi.mocked({ findOne: vi.fn() } as unknown as Repository); const db = { getLatestVersions: vi.fn(), getBundleFromVersions: vi.fn(), + getLatestBundleByEnv: vi.fn(), saveBundle: vi.fn(), } as unknown as BundleDatabase; const bundleDbMock = vi.mocked(db); @@ -67,7 +67,7 @@ describe('job.ts', function () { }); it('should create a bundle if no bundle exists', async function () { - bundleRepoMock.findOne.mockResolvedValueOnce(null); + bundleDbMock.getLatestBundleByEnv.mockResolvedValueOnce(null); bundleDbMock.getLatestVersions.mockResolvedValue({ assets: [{ name: 'avi', version: 1 }], environment: Environment.NP, @@ -75,7 +75,7 @@ describe('job.ts', function () { keyVersion: 1, }); - const promise = getJob(bundleRepoMock, bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); + const promise = getJob(bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); await expect(promise).resolves.not.toThrow(); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -83,13 +83,16 @@ describe('job.ts', function () { }); it('should create a bundle if the data versions changed', async function () { - bundleRepoMock.findOne.mockResolvedValueOnce({ + bundleDbMock.getLatestBundleByEnv.mockResolvedValueOnce({ id: 1, assets: [{ name: 'avi', version: 1 }], environment: Environment.NP, connections: [{ name: 'avi', version: 1 }], keyVersion: 2, opaVersion: '0.52.0', + createdAt: new Date(), + hash: 'avi', + metadata: null, }); bundleDbMock.getLatestVersions.mockResolvedValue({ assets: [{ name: 'avi', version: 1 }], @@ -98,7 +101,7 @@ describe('job.ts', function () { keyVersion: 1, }); - const promise = getJob(bundleRepoMock, bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); + const promise = getJob(bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); await expect(promise).resolves.not.toThrow(); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -106,7 +109,7 @@ describe('job.ts', function () { }); it('should create a bundle and not save the metadata to the db if there is a mismatch between s3 and the db', async function () { - bundleRepoMock.findOne.mockResolvedValueOnce({ + bundleDbMock.getLatestBundleByEnv.mockResolvedValueOnce({ id: 1, assets: [{ name: 'avi', version: 1 }], environment: Environment.NP, @@ -114,6 +117,8 @@ describe('job.ts', function () { keyVersion: 1, hash: 'avi', opaVersion: '0.52.0', + createdAt: new Date(), + metadata: null, }); bundleDbMock.getLatestVersions.mockResolvedValue({ assets: [{ name: 'avi', version: 1 }], @@ -122,7 +127,7 @@ describe('job.ts', function () { keyVersion: 1, }); - const promise = getJob(bundleRepoMock, bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); + const promise = getJob(bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); await expect(promise).resolves.not.toThrow(); expect(createBundle).toHaveBeenCalled(); @@ -133,14 +138,16 @@ describe('job.ts', function () { it('should not create a bundle if the bundle in s3 is up to date', async function () { // eslint-disable-next-line @typescript-eslint/naming-convention const res = await s3client.send(new PutObjectCommand({ Bucket: cronOptions.s3.bucket, Key: cronOptions.s3.key })); - bundleRepoMock.findOne.mockResolvedValueOnce({ + bundleDbMock.getLatestBundleByEnv.mockResolvedValueOnce({ id: 1, assets: [{ name: 'avi', version: 1 }], environment: Environment.NP, connections: [{ name: 'avi', version: 1 }], keyVersion: 1, - hash: res.ETag, + hash: res.ETag as string, opaVersion: '0.52.0', + createdAt: new Date(), + metadata: null, }); bundleDbMock.getLatestVersions.mockResolvedValue({ assets: [{ name: 'avi', version: 1 }], @@ -149,7 +156,7 @@ describe('job.ts', function () { keyVersion: 1, }); - const promise = getJob(bundleRepoMock, bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); + const promise = getJob(bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); await expect(promise).resolves.not.toThrow(); // eslint-disable-next-line @typescript-eslint/unbound-method @@ -159,14 +166,16 @@ describe('job.ts', function () { it('should create a bundle if the opa version is different than the one in the db', async function () { // eslint-disable-next-line @typescript-eslint/naming-convention const res = await s3client.send(new PutObjectCommand({ Bucket: cronOptions.s3.bucket, Key: cronOptions.s3.key })); - bundleRepoMock.findOne.mockResolvedValueOnce({ + bundleDbMock.getLatestBundleByEnv.mockResolvedValueOnce({ id: 1, assets: [{ name: 'avi', version: 1 }], environment: Environment.NP, connections: [{ name: 'avi', version: 1 }], keyVersion: 1, - hash: res.ETag, + hash: res.ETag as string, opaVersion: '0.51.0', + createdAt: new Date(), + metadata: null, }); bundleDbMock.getLatestVersions.mockResolvedValue({ assets: [{ name: 'avi', version: 1 }], @@ -175,7 +184,7 @@ describe('job.ts', function () { keyVersion: 1, }); - const promise = getJob(bundleRepoMock, bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); + const promise = getJob(bundleDbMock, Environment.NP, path.join(tmpdir(), 'authcrontests'))(); await expect(promise).resolves.not.toThrow(); // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/apps/auth-cron/tests/s3.spec.mts b/apps/auth-cron/tests/s3.spec.mts index d4a1e28c..66f8473c 100644 --- a/apps/auth-cron/tests/s3.spec.mts +++ b/apps/auth-cron/tests/s3.spec.mts @@ -80,7 +80,7 @@ describe('s3.ts', function () { }); it('should return undefined if the object does not exists', async function () { - const hash = await getS3Client(Environment.PRODUCTION).getObjectHash(); + const hash = await getS3Client(Environment.PROD).getObjectHash(); expect(hash).toBeUndefined(); }); @@ -100,7 +100,7 @@ describe('s3.ts', function () { }); it('should return false if the bucket does not exist', async function () { - const res = await getS3Client(Environment.PRODUCTION).doesBucketExist(); + const res = await getS3Client(Environment.PROD).doesBucketExist(); expect(res).toBe(false); }); diff --git a/packages/auth-bundler/dataSource.ts b/packages/auth-bundler/dataSource.ts deleted file mode 100644 index 9db15a74..00000000 --- a/packages/auth-bundler/dataSource.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { DataSource } from 'typeorm'; -import config from 'config'; -import { createConnectionOptions } from '@map-colonies/auth-core'; -import { commonDbFullV1Type } from '@map-colonies/schemas'; - -const connectionOptions = config.util.toObject() as commonDbFullV1Type; - -export const appDataSource = new DataSource({ - ...createConnectionOptions(connectionOptions), -}); diff --git a/packages/auth-bundler/package.json b/packages/auth-bundler/package.json index e6e4e181..2f1f7460 100644 --- a/packages/auth-bundler/package.json +++ b/packages/auth-bundler/package.json @@ -42,7 +42,7 @@ "execa": "^7.1.1", "handlebars": "4.7.7", "pg": "catalog:", - "typeorm": "catalog:" + "drizzle-orm": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", @@ -58,8 +58,7 @@ "vitest": "catalog:", "@vitest/coverage-v8": "catalog:", "@vitest/ui": "catalog:", - "@map-colonies/vitest-utils": "catalog:", - "@faker-js/faker": "catalog:" + "@map-colonies/vitest-utils": "catalog:" }, "peerDependencies": { "@map-colonies/js-logger": "catalog:" diff --git a/packages/auth-bundler/src/bundler.ts b/packages/auth-bundler/src/bundler.ts index a6fcae3b..7d189f71 100644 --- a/packages/auth-bundler/src/bundler.ts +++ b/packages/auth-bundler/src/bundler.ts @@ -6,7 +6,7 @@ import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { Asset, AssetTypes, Key } from '@map-colonies/auth-core'; +import type { Asset, AssetType, Key } from '@map-colonies/auth-core'; import { render } from './templating'; import type { BundleContent } from './types'; import { logger } from './logger'; @@ -46,7 +46,7 @@ async function handleKey(basePath: string, key: Key): Promise { * @ignore */ async function handleAsset(basePath: string, asset: Asset, context: unknown): Promise { - let value = Buffer.from(asset.value, 'base64').toString('utf-8'); + let value = asset.value.toString('utf-8'); if (asset.isTemplate) { value = render(value, context); @@ -75,7 +75,7 @@ async function handleAsset(basePath: string, asset: Asset, context: unknown): Pr * @ignore */ export async function createBundleDirectoryStructure(bundle: BundleContent, path: string): Promise { - const hasAssetType: Record = { + const hasAssetType: Record = { /* eslint-disable @typescript-eslint/naming-convention */ DATA: false, POLICY: false, diff --git a/packages/auth-bundler/src/db.ts b/packages/auth-bundler/src/db.ts index daac0a88..c2160f82 100644 --- a/packages/auth-bundler/src/db.ts +++ b/packages/auth-bundler/src/db.ts @@ -1,35 +1,66 @@ -import type { DataSource, Repository } from 'typeorm'; -import { assetTable, Bundle, Connection, type Environments, Key } from '@map-colonies/auth-core'; +import type { Pool } from 'pg'; +import { sql, type SQL, type AnyColumn, and, arrayContains, max, eq } from 'drizzle-orm'; +import { assetTable, createDrizzle, bundleTable, connectionTable, keyTable } from '@map-colonies/auth-core'; +import type { Environments, NewBundle, Drizzle, Bundle } from '@map-colonies/auth-core'; import type { BundleContent, BundleContentVersions } from './types'; import { extractNameAndVersion } from './util'; import { logger } from './logger'; -import { ConnectionNotInitializedError, KeyNotFoundError } from './errors'; +import { KeyNotFoundError } from './errors'; import { getVersionCommand } from './opa'; +// TypeScript Magic: Maps an array of columns to an array of *arrays* of those column types +// [Column, Column] becomes [number[], string[]] +type InferTransposedArrays = { + [K in keyof T]: T[K] extends AnyColumn ? T[K]['_']['data'][] : never; +}; + +/** + * A composite IN filter using Postgres unnest() with explicit type casting. + */ +function inCompositeUnnest(columns: [...T], transposedValues: [...InferTransposedArrays]): SQL { + // Guard clause + if (transposedValues[0].length === 0) { + return sql`false`; + } + + const columnList = sql.join( + columns.map((col) => sql`${col}`), + sql`, ` + ); + + // Map each array parameter to its explicit Postgres array type cast + const unnestArgs = sql.join( + transposedValues.map((arr, index) => { + const col = columns[index]; + let sqlType = col.getSQLType(); + + // Postgres pseudo-types handling (e.g., 'serial' cannot be cast as 'serial[]') + if (sqlType === 'serial') sqlType = 'integer'; + if (sqlType === 'bigserial') sqlType = 'bigint'; + if (sqlType === 'smallserial') sqlType = 'smallint'; + + // Safely append [] to the SQL type string using sql.raw + return sql`${sql.param(arr)}::${sql.raw(sqlType)}[]`; + }), + sql`, ` + ); + + return sql`(${columnList}) IN (SELECT * FROM unnest(${unnestArgs}))`; +} + /** * This class handles all the database interactions required to creating a bundle. */ export class BundleDatabase { - private readonly assetRepository: Repository; - private readonly keyRepository: Repository; - private readonly connectionRepository: Repository; - private readonly bundleRepository: Repository; + private readonly drizzle: Drizzle; /** * Initializes the class for communication with the database. * The dataSource should point to a database initialized with the model defined in the auth-core package. - * @param dataSource The typeorm dataSource to use in the class - * @see {@link https://typeorm.io/data-source} * @throws {@link ConnectionNotInitializedError} If the dataSource is not initialized. */ - public constructor(private readonly dataSource: DataSource) { - if (!dataSource.isInitialized) { - throw new ConnectionNotInitializedError('DB connection it not initialized'); - } - this.assetRepository = dataSource.getRepository(assetTable); - this.keyRepository = dataSource.getRepository(Key); - this.connectionRepository = dataSource.getRepository(Connection); - this.bundleRepository = dataSource.getRepository(Bundle); + public constructor(pool: Pool) { + this.drizzle = createDrizzle(pool); } /** @@ -47,6 +78,14 @@ export class BundleDatabase { }; } + public async getLatestBundleByEnv(env: Environments): Promise { + const bundle = await this.drizzle.query.bundle.findFirst({ + where: { environment: env }, + orderBy: { id: 'desc' }, + }); + return bundle ?? null; + } + /** * Saved the metadata of the bundle into the database * @param versions The versions of the bundle content @@ -55,7 +94,7 @@ export class BundleDatabase { */ public async saveBundle(versions: BundleContentVersions, hash: string): Promise { logger?.debug('saving bundle to db'); - const bundle: Omit = { + const bundle: NewBundle = { environment: versions.environment, assets: versions.assets, connections: versions.connections, @@ -64,8 +103,8 @@ export class BundleDatabase { opaVersion: await getVersionCommand(), }; - const res = await this.bundleRepository.save(bundle); - return res.id; + const res = await this.drizzle.insert(bundleTable).values(bundle).returning(); + return res[0]?.id as number; } /** @@ -76,74 +115,84 @@ export class BundleDatabase { public async getBundleFromVersions(versions: BundleContentVersions): Promise { logger?.debug('fetching bundle from the db'); - const assets = this.dataSource - .getRepository(assetTable) - .createQueryBuilder() - .whereInIds(extractNameAndVersion(versions.assets)) - .andWhere(':env = ANY(environment)', { env: versions.environment }) - .getMany(); - - const connections = this.dataSource - .getRepository(Connection) - .createQueryBuilder() - .whereInIds(extractNameAndVersion(versions.connections)) - .andWhere(':env = environment', { env: versions.environment }) - .getMany(); - - const key = + const assetsQuery = this.drizzle + .select() + .from(assetTable) + .where( + and( + inCompositeUnnest([assetTable.name, assetTable.version], extractNameAndVersion(versions.assets)), + arrayContains(assetTable.environment, [versions.environment]) + ) + ); + + const connectionsQuery = this.drizzle + .select() + .from(connectionTable) + .where( + and( + inCompositeUnnest([connectionTable.name, connectionTable.version], extractNameAndVersion(versions.connections)), + sql`${connectionTable.environment} = ${versions.environment}` + ) + ); + + const keyQuery = versions.keyVersion !== undefined - ? this.dataSource.getRepository(Key).findOneByOrFail({ environment: versions.environment, version: versions.keyVersion }) + ? this.drizzle.query.key.findFirst({ where: { environment: versions.environment, version: versions.keyVersion } }) : undefined; - const promises = await Promise.all([assets, connections, key]); + const assets = await assetsQuery; + const connections = await connectionsQuery; + const key = await keyQuery; + const promises = [assets, connections, key] as const; return { assets: promises[0], connections: promises[1], key: promises[2], environment: versions.environment }; } - private async getAssetsVersions(environment: string): Promise<{ name: string; version: number }[]> { - const subQuery = this.assetRepository - .createQueryBuilder('asset') - .select(['name', 'MAX(version)']) - .where(':environment = ANY (environment)', { environment }) - .groupBy('name'); - - return this.assetRepository - .createQueryBuilder('asset') - .select(['name', 'version']) - .where(':environment = ANY (environment)', { environment }) - .andWhere('(name, version) IN (' + subQuery.getQuery() + ')') - .orderBy('name') - .getRawMany<{ name: string; version: number }>(); + private async getAssetsVersions(environment: Environments): Promise<{ name: string; version: number }[]> { + const subQuery = this.drizzle + .select({ name: assetTable.name, version: max(assetTable.version) }) + .from(assetTable) + .where(arrayContains(assetTable.environment, [environment])) + .groupBy(assetTable.name); + + return this.drizzle + .select({ name: assetTable.name, version: assetTable.version }) + .from(assetTable) + .where(and(arrayContains(assetTable.environment, [environment]), sql`(${assetTable.name}, ${assetTable.version}) IN (${subQuery})`)) + .orderBy(assetTable.name); } - private async getLatestKeyVersion(environment: string): Promise { - const res = await this.keyRepository - .createQueryBuilder('key') - .select('MAX(version) as version') - .where('environment = :environment', { environment }) - .getRawOne<{ version: number | null }>(); + private async getLatestKeyVersion(environment: Environments): Promise { + const res = await this.drizzle + .select({ version: max(keyTable.version) }) + .from(keyTable) + .where(eq(keyTable.environment, environment)); - if (res === undefined) { + if (res[0] === undefined) { throw new KeyNotFoundError(`couldn't not find a key for environment: ${environment}`); } - return res.version; + return res[0].version; } - private async getConnectionsVersions(environment: string): Promise<{ name: string; version: number }[]> { - const subQuery = this.connectionRepository - .createQueryBuilder('connection') - .select(['name', 'MAX(version)']) - .where(':environment = environment', { environment }) - .groupBy('name'); - - return this.connectionRepository - .createQueryBuilder('connection') - .select(['name', 'version']) - .where(':environment = environment AND enabled = TRUE') - .andWhere('(name, version) IN (' + subQuery.getQuery() + ')') - .orderBy('name') - .setParameters(subQuery.getParameters()) - .getRawMany<{ name: string; version: number }>(); + private async getConnectionsVersions(environment: Environments): Promise<{ name: string; version: number }[]> { + const subQuery = this.drizzle + .select({ name: connectionTable.name, version: max(connectionTable.version) }) + .from(connectionTable) + .where(eq(connectionTable.environment, environment)) + .groupBy(connectionTable.name) + .$dynamic(); + + return this.drizzle + .select({ name: connectionTable.name, version: connectionTable.version }) + .from(connectionTable) + .where( + and( + eq(connectionTable.environment, environment), + sql`(${connectionTable.name}, ${connectionTable.version}) IN (${subQuery})`, + eq(connectionTable.enabled, true) + ) + ) + .orderBy(connectionTable.name); } } diff --git a/packages/auth-bundler/src/errors.ts b/packages/auth-bundler/src/errors.ts index 454aa90a..683eb3fe 100644 --- a/packages/auth-bundler/src/errors.ts +++ b/packages/auth-bundler/src/errors.ts @@ -5,12 +5,12 @@ export class MissingPolicyFilesError extends Error { } } -export class ConnectionNotInitializedError extends Error { - public constructor(message: string) { - super(message); - Object.setPrototypeOf(this, ConnectionNotInitializedError.prototype); - } -} +// export class ConnectionNotInitializedError extends Error { +// public constructor(message: string) { +// super(message); +// Object.setPrototypeOf(this, ConnectionNotInitializedError.prototype); +// } +// } export class KeyNotFoundError extends Error { public constructor(message: string) { diff --git a/packages/auth-bundler/src/util.ts b/packages/auth-bundler/src/util.ts index e7616876..1b897203 100644 --- a/packages/auth-bundler/src/util.ts +++ b/packages/auth-bundler/src/util.ts @@ -1,3 +1,9 @@ -export function extractNameAndVersion(entities: T[]): { name: string; version: number }[] { - return entities.map((a) => ({ name: a.name, version: a.version })); +export function extractNameAndVersion(entities: T[]): [string[], number[]] { + const names: string[] = []; + const versions: number[] = []; + for (const entity of entities) { + names.push(entity.name); + versions.push(entity.version); + } + return [names, versions]; } diff --git a/packages/auth-bundler/tests/bundler.spec.mts b/packages/auth-bundler/tests/bundler.spec.mts index 7c44b4e2..1ee85be7 100644 --- a/packages/auth-bundler/tests/bundler.spec.mts +++ b/packages/auth-bundler/tests/bundler.spec.mts @@ -34,7 +34,7 @@ describe('bundler.ts', function () { it('should throw an error if there are no policy files', async function () { const content: BundleContent = { - environment: Environment.PRODUCTION, + environment: Environment.PROD, assets: [], connections: [], }; @@ -55,7 +55,7 @@ describe('bundler.ts', function () { setLogger(logger); const content: BundleContent = { - environment: Environment.PRODUCTION, + environment: Environment.PROD, assets: [bundleContent.assets[1]!], connections: [], }; diff --git a/packages/auth-bundler/tests/db.spec.ts b/packages/auth-bundler/tests/db.spec.mts similarity index 50% rename from packages/auth-bundler/tests/db.spec.ts rename to packages/auth-bundler/tests/db.spec.mts index 25ef91b3..bcb93ef8 100644 --- a/packages/auth-bundler/tests/db.spec.ts +++ b/packages/auth-bundler/tests/db.spec.mts @@ -1,15 +1,22 @@ /// -import { assetTable, Bundle, Connection, Environment, Key, createConnectionOptions } from '@map-colonies/auth-core'; -import { DataSource } from 'typeorm'; +import { + assetTable, + Environment, + initConnection, + createDrizzle, + type Drizzle, + keyTable, + connectionTable, + bundleTable, + type Asset, +} from '@map-colonies/auth-core'; +import type { Pool } from 'pg'; import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest'; -import { ConnectionNotInitializedError } from '@src/index'; -import { BundleDatabase } from '@src/db'; -import * as execa from '@src/wrappers/execa'; -import { getMockKeys } from './utils/key'; -import { getFakeAsset } from './utils/asset'; -import { getFakeConnection } from './utils/connection'; -import { getConfig, initConfig } from './helpers/config'; +import { getFakeAsset, getFakeConnection, getMockKeys } from 'test-utils'; +import { BundleDatabase } from '@src/db.js'; +import * as execa from '@src/wrappers/execa.js'; +import { getConfig, initConfig } from './helpers/config.js'; vi.mock('../src/wrappers/execa', async () => { return { @@ -22,49 +29,34 @@ vi.mock('../src/wrappers/execa', async () => { type ExecaChildProcess = Awaited>; describe('db.ts', function () { - let dataSource: DataSource; - const asset = { ...getFakeAsset(), environment: [Environment.PRODUCTION], name: 'aviaviavi' }; - const connection: Connection = { ...getFakeConnection(), environment: Environment.PRODUCTION, name: 'xd' }; + let pool: Pool; + let drizzle: Drizzle; + const asset = getFakeAsset(false, { environment: [Environment.PROD], name: 'aviaviavi' }); + const connection = getFakeConnection(false, { environment: Environment.PROD, name: 'xd' }); beforeAll(async function () { await initConfig(); const connectionOptions = getConfig().getAll(); - dataSource = new DataSource({ - ...createConnectionOptions(connectionOptions), - }); - - await dataSource.initialize(); + pool = await initConnection(connectionOptions); + drizzle = createDrizzle(pool); const [privateKey, publicKey] = getMockKeys(); - await dataSource.getRepository(Key).save({ environment: Environment.PRODUCTION, version: 1, privateKey, publicKey }); + await drizzle.insert(keyTable).values({ environment: Environment.PROD, version: 1, privateKey, publicKey }); - await dataSource.getRepository(assetTable).save([ + await drizzle.insert(assetTable).values([ { ...asset, version: 1 }, { ...asset, version: 2 }, ]); - await dataSource.getRepository(Connection).save([ + await drizzle.insert(connectionTable).values([ { ...connection, version: 1 }, { ...connection, version: 2 }, ]); }); afterAll(async function () { - await dataSource.destroy(); - }); - - describe('#init', function () { - it('should throw an error if datasource is not initialized', function () { - const connectionOptions = getConfig().getAll(); - const dataSource = new DataSource({ - ...createConnectionOptions(connectionOptions), - }); - - expect(() => { - new BundleDatabase(dataSource); - }).toThrow(ConnectionNotInitializedError); - }); + await pool.end(); }); describe('#saveBundle', function () { @@ -72,16 +64,17 @@ describe('db.ts', function () { const VERSION_OUTPUT = 'Version: 0.52.0\nBuild Commit: 8d2c137662560cac83d9cf24cbdaecc934910333\nBuild Timestamp: 2023-04-27T17:57:23Z'; vi.spyOn(execa, 'execa').mockResolvedValue({ stdout: VERSION_OUTPUT } as ExecaChildProcess); - const db = new BundleDatabase(dataSource); + const db = new BundleDatabase(pool); - const res = await db.saveBundle({ assets: [], connections: [], environment: Environment.PRODUCTION, keyVersion: 3 }, 'xdxd'); + const res = await db.saveBundle({ assets: [], connections: [], environment: Environment.PROD, keyVersion: 3 }, 'xdxd'); expect(res).toBeGreaterThan(0); - const bundle = await dataSource.getRepository(Bundle).findOneByOrFail({ id: res }); + // const bundle = await pool.getRepository(Bundle).findOneByOrFail({ id: res }); + const bundle = await drizzle.query.bundle.findFirst({ where: { id: res } }); expect(bundle).toMatchObject({ - environment: Environment.PRODUCTION, + environment: Environment.PROD, keyVersion: 3, opaVersion: '0.52.0', hash: 'xdxd', @@ -92,9 +85,9 @@ describe('db.ts', function () { describe('#getLatestVersions', function () { it('should fetch the latest versions from the database', async function () { - const db = new BundleDatabase(dataSource); + const db = new BundleDatabase(pool); - const { assets, connections, keyVersion } = await db.getLatestVersions(Environment.PRODUCTION); + const { assets, connections, keyVersion } = await db.getLatestVersions(Environment.PROD); expect(keyVersion).toBe(1); @@ -110,12 +103,12 @@ describe('db.ts', function () { }); it('should return the latest version of the asset even if there is a newer version in the database with a different environment', async function () { - await dataSource.getRepository(assetTable).save([ + await drizzle.insert(assetTable).values([ { ...asset, name: 'xd', environment: [Environment.STAGE, Environment.NP], version: 1 }, { ...asset, name: 'xd', environment: [Environment.STAGE], version: 2 }, ]); - const { assets } = await new BundleDatabase(dataSource).getLatestVersions(Environment.NP); + const { assets } = await new BundleDatabase(pool).getLatestVersions(Environment.NP); expect(assets).toHaveLength(1); expect(assets[0]).toMatchObject({ name: 'xd', version: 1 }); @@ -124,18 +117,49 @@ describe('db.ts', function () { describe('#getBundleFromVersions', function () { it('should fetch the bundle content based on the versions from the database', async function () { - const db = new BundleDatabase(dataSource); + const db = new BundleDatabase(pool); const { assets } = await db.getBundleFromVersions({ - environment: Environment.PRODUCTION, + environment: Environment.PROD, assets: [{ name: 'aviaviavi', version: 1 }], connections: [], keyVersion: 1, }); - expect(assets).toSatisfyAll((a) => a.environment.includes(Environment.PRODUCTION)); + expect(assets).toSatisfyAll((a) => a.environment.includes(Environment.PROD)); expect(assets[0]).toHaveProperty('version', 1); }); }); }); + + describe('#getLatestBundleByEnv', function () { + it('should return null when no bundle exists for the environment', async function () { + const db = new BundleDatabase(pool); + + const result = await db.getLatestBundleByEnv(Environment.STAGE); + + expect(result).toBeNull(); + }); + + it('should return the latest bundle ordered by id for the given environment', async function () { + await drizzle.insert(bundleTable).values([ + { environment: Environment.STAGE, hash: 'hash1', opaVersion: '0.52.0', assets: [], connections: [] }, + { environment: Environment.STAGE, hash: 'hash2', opaVersion: '0.52.0', assets: [], connections: [] }, + ]); + + const db = new BundleDatabase(pool); + const result = await db.getLatestBundleByEnv(Environment.STAGE); + + expect(result).not.toBeNull(); + expect(result).toMatchObject({ environment: Environment.STAGE, hash: 'hash2' }); + }); + + it('should not return bundles from a different environment', async function () { + const db = new BundleDatabase(pool); + + const result = await db.getLatestBundleByEnv(Environment.NP); + + expect(result).toBeNull(); + }); + }); }); diff --git a/packages/auth-bundler/tests/util.spec.ts b/packages/auth-bundler/tests/util.spec.ts index 7ef8d2fc..97dc1885 100644 --- a/packages/auth-bundler/tests/util.spec.ts +++ b/packages/auth-bundler/tests/util.spec.ts @@ -12,8 +12,8 @@ describe('util.ts', function () { const res = extractNameAndVersion(input); expect(res).toStrictEqual([ - { name: 'avi', version: 1 }, - { name: 'iva', version: 1 }, + ['avi', 'iva'], + [1, 1], ]); }); }); diff --git a/packages/auth-bundler/tests/utils/asset.ts b/packages/auth-bundler/tests/utils/asset.ts deleted file mode 100644 index add7903d..00000000 --- a/packages/auth-bundler/tests/utils/asset.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { faker } from '@faker-js/faker'; -import { AssetType, Environment, type IAsset } from '@map-colonies/auth-core'; - -const EIGHT = 8; - -export function getFakeAsset(includeCreated?: boolean): IAsset { - return { - createdAt: includeCreated === true ? faker.date.past() : undefined, - environment: [Environment.NP], - isTemplate: faker.datatype.boolean(), - name: faker.string.sample(EIGHT), - type: faker.helpers.arrayElement(Object.values(AssetType)), - uri: faker.system.filePath(), - value: Buffer.from(faker.lorem.paragraph()).toString('base64'), - version: 1, - }; -} diff --git a/packages/auth-bundler/tests/utils/bundle.ts b/packages/auth-bundler/tests/utils/bundle.ts index 789a334f..aef0ff10 100644 --- a/packages/auth-bundler/tests/utils/bundle.ts +++ b/packages/auth-bundler/tests/utils/bundle.ts @@ -1,7 +1,7 @@ import { AssetType, Environment } from '@map-colonies/auth-core'; import type { BundleContent } from '@src/index'; -const baseAsset = { createdAt: new Date(), environment: [Environment.PRODUCTION], version: 1 }; +const baseAsset = { createdAt: new Date(), environment: [Environment.PROD], version: 1 }; const policy = Buffer.from( ` @@ -9,7 +9,7 @@ allow { true } ` -).toString('base64'); +); const test = Buffer.from( ` @@ -17,13 +17,13 @@ test_allow { true } ` -).toString('base64'); +); -const data = Buffer.from(`{{#delimitedEach .}}{{name}}{{/delimitedEach}}`).toString('base64'); +const data = Buffer.from(`{{#delimitedEach .}}{{name}}{{/delimitedEach}}`); export function getFakeBundleContent(): BundleContent { return { - environment: Environment.PRODUCTION, + environment: Environment.PROD, assets: [ { ...baseAsset, @@ -57,7 +57,7 @@ export function getFakeBundleContent(): BundleContent { createdAt: new Date(), domains: [], enabled: true, - environment: Environment.PRODUCTION, + environment: Environment.PROD, name: 'avi', origins: [], token: '', @@ -65,7 +65,7 @@ export function getFakeBundleContent(): BundleContent { }, ], key: { - environment: Environment.PRODUCTION, + environment: Environment.PROD, version: 1, publicKey: { alg: 'a', e: 'a', kid: 'a', kty: 'a', n: 'a' }, privateKey: { alg: 'a', e: 'a', kid: 'a', kty: 'a', n: 'a', d: 'a', dp: 'a', dq: 'a', p: 'a', q: 'a', qi: 'a' }, diff --git a/packages/auth-bundler/tests/utils/connection.ts b/packages/auth-bundler/tests/utils/connection.ts deleted file mode 100644 index d36fcd7a..00000000 --- a/packages/auth-bundler/tests/utils/connection.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type Connection, Environment } from '@map-colonies/auth-core'; -import { faker } from '@faker-js/faker'; - -const EIGHT = 8; - -export function getFakeConnection(): Connection { - return { - createdAt: faker.date.past(), - environment: Environment.NP, - version: 1, - name: faker.string.sample(EIGHT), - allowNoBrowserConnection: faker.datatype.boolean(), - allowNoOriginConnection: faker.datatype.boolean(), - domains: ['alpha', 'bravo'], - origins: ['c', 'd'], - enabled: true, - token: faker.string.alpha(), - }; -} diff --git a/packages/auth-bundler/tests/utils/key.ts b/packages/auth-bundler/tests/utils/key.ts deleted file mode 100644 index 8cd0cd53..00000000 --- a/packages/auth-bundler/tests/utils/key.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { faker } from '@faker-js/faker'; -import type { JWKPrivateKey, JWKPublicKey } from '@map-colonies/auth-core'; - -const LENGTH_OF_STRING = 3; - -export function getMockKeys(): [JWKPrivateKey, JWKPublicKey] { - const publicKey: JWKPublicKey = { - alg: faker.string.alpha(LENGTH_OF_STRING), - e: faker.string.alpha(LENGTH_OF_STRING), - kid: faker.string.alpha(LENGTH_OF_STRING), - kty: faker.string.alpha(LENGTH_OF_STRING), - n: faker.string.alpha(LENGTH_OF_STRING), - }; - return [ - { - ...publicKey, - d: faker.string.alpha(LENGTH_OF_STRING), - dp: faker.string.alpha(LENGTH_OF_STRING), - dq: faker.string.alpha(LENGTH_OF_STRING), - p: faker.string.alpha(LENGTH_OF_STRING), - q: faker.string.alpha(LENGTH_OF_STRING), - qi: faker.string.alpha(LENGTH_OF_STRING), - }, - publicKey, - ]; -} diff --git a/packages/test-utils/src/fakers.ts b/packages/test-utils/src/fakers.ts index 8a7c9ea6..d7c15308 100644 --- a/packages/test-utils/src/fakers.ts +++ b/packages/test-utils/src/fakers.ts @@ -16,9 +16,9 @@ import { AssetType, Environment } from '@map-colonies/auth-core'; const EIGHT = 8; const THREE = 3; -export function getFakeAsset(includeCreated: true): Asset; -export function getFakeAsset(includeCreated?: false): NewAsset; -export function getFakeAsset(includeCreated?: boolean): Asset | NewAsset { +export function getFakeAsset(includeCreated: true, override?: Partial): Asset; +export function getFakeAsset(includeCreated?: false, override?: Partial): NewAsset; +export function getFakeAsset(includeCreated?: boolean, override?: Partial): Asset | NewAsset { return { createdAt: includeCreated === true ? faker.date.past() : undefined, environment: [Environment.NP], @@ -28,6 +28,7 @@ export function getFakeAsset(includeCreated?: boolean): Asset | NewAsset { uri: faker.system.filePath(), value: Buffer.from(faker.lorem.paragraph()), version: 1, + ...override, }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8a210b9..a40e514e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ catalogs: tsyringe: specifier: ^4.10.0 version: 4.10.0 - typeorm: - specifier: ^0.3.12 - version: 0.3.28 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -266,6 +263,9 @@ importers: croner: specifier: 6.0.3 version: 6.0.3 + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) express: specifier: 'catalog:' version: 4.22.1 @@ -278,9 +278,6 @@ importers: prom-client: specifier: 'catalog:' version: 15.1.3 - typeorm: - specifier: 'catalog:' - version: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)) devDependencies: '@map-colonies/eslint-config': specifier: 'catalog:' @@ -300,6 +297,9 @@ importers: '@types/node': specifier: 'catalog:' version: 24.12.0 + '@types/pg': + specifier: 'catalog:' + version: 8.20.0 '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.4(vitest@4.1.4) @@ -463,9 +463,6 @@ importers: test-utils: specifier: workspace:^ version: link:../../packages/test-utils - type-fest: - specifier: ^4.40.0 - version: 4.41.0 typescript: specifier: 'catalog:' version: 5.9.3 @@ -919,6 +916,9 @@ importers: '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) + drizzle-orm: + specifier: 'catalog:' + version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) execa: specifier: ^7.1.1 version: 7.2.0 @@ -928,13 +928,7 @@ importers: pg: specifier: 'catalog:' version: 8.20.0 - typeorm: - specifier: 'catalog:' - version: 0.3.28(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)) devDependencies: - '@faker-js/faker': - specifier: 'catalog:' - version: 9.9.0 '@map-colonies/config': specifier: 'catalog:' version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) @@ -4115,9 +4109,6 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@sqltools/formatter@1.2.5': - resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -4953,10 +4944,6 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} - engines: {node: '>=14'} - any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -4964,10 +4951,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - app-root-path@3.1.0: - resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} - engines: {node: '>= 6.0.0'} - append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -5619,9 +5602,6 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -5757,10 +5737,6 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - drizzle-kit@1.0.0-rc.2: resolution: {integrity: sha512-TRxUmj1wDA2QCt3GvuhfamvIa66wJ7+MzSxBMKkpRtYScjHTumT9BE+x6daSzuEacSrPEuUH5/cW1uo5RkoPIg==} hasBin: true @@ -8531,11 +8507,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha.js@2.4.12: - resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} - engines: {node: '>= 0.10'} - hasBin: true - shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} @@ -8657,10 +8628,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sql-highlight@6.1.0: - resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} - engines: {node: '>=14'} - ssh-remote-port-forward@1.0.4: resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} @@ -8944,10 +8911,6 @@ packages: tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-buffer@1.2.2: - resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} - engines: {node: '>= 0.4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -9087,61 +9050,6 @@ packages: typeof@1.0.0: resolution: {integrity: sha512-Pze0mIxYXhaJdpw1ayMzOA7rtGr1OmsTY/Z+FWtRKIqXFz6aoDLjqdbWE/tcIBSC8nhnVXiRrEXujodR/xiFAA==} - typeorm@0.3.28: - resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==} - engines: {node: '>=16.13.0'} - hasBin: true - peerDependencies: - '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@sap/hana-client': ^2.14.22 - better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 - ioredis: ^5.0.4 - mongodb: ^5.8.0 || ^6.0.0 - mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 - mysql2: ^2.2.5 || ^3.0.1 - oracledb: ^6.3.0 - pg: ^8.5.1 - pg-native: ^3.0.0 - pg-query-stream: ^4.0.0 - redis: ^3.1.1 || ^4.0.0 || ^5.0.14 - sql.js: ^1.4.0 - sqlite3: ^5.0.3 - ts-node: ^10.7.0 - typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 - peerDependenciesMeta: - '@google-cloud/spanner': - optional: true - '@sap/hana-client': - optional: true - better-sqlite3: - optional: true - ioredis: - optional: true - mongodb: - optional: true - mssql: - optional: true - mysql2: - optional: true - oracledb: - optional: true - pg: - optional: true - pg-native: - optional: true - pg-query-stream: - optional: true - redis: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - ts-node: - optional: true - typeorm-aurora-data-api-driver: - optional: true - typescript-eslint@8.57.2: resolution: {integrity: sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9285,10 +9193,6 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -13259,8 +13163,6 @@ snapshots: '@sinonjs/commons': 3.0.1 optional: true - '@sqltools/formatter@1.2.5': {} - '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -14260,8 +14162,6 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.2.0: {} - any-promise@1.3.0: {} anymatch@3.1.3: @@ -14269,8 +14169,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - app-root-path@3.1.0: {} - append-field@1.0.0: {} archiver-utils@5.0.2: @@ -15006,8 +14904,6 @@ snapshots: dateformat@4.6.3: {} - dayjs@1.11.20: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -15024,7 +14920,8 @@ snapshots: dependencies: mimic-response: 3.1.0 - dedent@1.7.2: {} + dedent@1.7.2: + optional: true deep-is@0.1.4: {} @@ -15120,8 +15017,6 @@ snapshots: dotenv@16.4.7: {} - dotenv@16.6.1: {} - drizzle-kit@1.0.0-rc.2: dependencies: '@drizzle-team/brocli': 0.11.0 @@ -18378,12 +18273,6 @@ snapshots: setprototypeof@1.2.0: {} - sha.js@2.4.12: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - shallowequal@1.1.0: {} shebang-command@2.0.0: @@ -18522,8 +18411,6 @@ snapshots: sprintf-js@1.0.3: optional: true - sql-highlight@6.1.0: {} - ssh-remote-port-forward@1.0.4: dependencies: '@types/ssh2': 0.5.52 @@ -18880,12 +18767,6 @@ snapshots: tmpl@1.0.5: optional: true - to-buffer@1.2.2: - dependencies: - isarray: 2.0.5 - safe-buffer: 5.2.1 - typed-array-buffer: 1.0.3 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -19052,30 +18933,6 @@ snapshots: typeof@1.0.0: {} - typeorm@0.3.28(pg@8.20.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)): - dependencies: - '@sqltools/formatter': 1.2.5 - ansis: 4.2.0 - app-root-path: 3.1.0 - buffer: 6.0.3 - dayjs: 1.11.20 - debug: 4.4.3(supports-color@10.2.2) - dedent: 1.7.2 - dotenv: 16.6.1 - glob: 10.5.0 - reflect-metadata: 0.2.2 - sha.js: 2.4.12 - sql-highlight: 6.1.0 - tslib: 2.8.1 - uuid: 11.1.0 - yargs: 17.7.2 - optionalDependencies: - pg: 8.20.0 - ts-node: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - typescript-eslint@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3): dependencies: '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3) @@ -19213,8 +19070,6 @@ snapshots: uuid@10.0.0: {} - uuid@11.1.0: {} - uuid@8.3.2: {} uuid@9.0.1: {} From 67331e9e54c32db7131b76bc5b8da7377c2687b3 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 27 May 2026 14:01:49 +0300 Subject: [PATCH 09/14] chore(global): done --- apps/auth-cron/package.json | 1 + apps/auth-cron/src/index.ts | 13 +- apps/auth-cron/tests/job.spec.mts | 2 - apps/auth-cron/tests/validators.spec.mts | 2 +- apps/auth-manager/package.json | 1 + .../client/controllers/clientController.ts | 3 +- .../src/client/models/clientManager.ts | 13 +- apps/auth-manager/src/common/db/constants.ts | 4 - apps/auth-manager/src/common/db/pagination.ts | 21 --- apps/auth-manager/src/common/db/sort.ts | 33 ---- apps/auth-manager/src/common/db/utils.ts | 34 ----- apps/auth-manager/src/common/errors.ts | 13 -- .../src/common/utils/promiseTimeout.ts | 14 -- .../controllers/connectionController.ts | 3 +- .../connection/models/connectionManager.ts | 5 +- apps/auth-manager/src/containerConfig.ts | 17 +-- .../domain/controllers/domainController.ts | 3 +- .../src/domain/models/domainManager.ts | 5 +- apps/auth-manager/src/runMigrations.mts | 10 ++ .../configurations/vitest.globalSetup.mts | 2 +- .../tests/integration/asset/asset.spec.mts | 4 - .../connection/connection.spec.mts | 4 +- .../unit/client/models/clientManager.spec.mts | 2 +- apps/token-kiosk/drizzle.config.mts | 2 +- apps/token-kiosk/package.json | 6 +- apps/token-kiosk/src/common/constants.ts | 2 - apps/token-kiosk/src/common/utils.ts | 14 -- apps/token-kiosk/src/containerConfig.ts | 5 +- apps/token-kiosk/src/db/createConnection.ts | 73 --------- apps/token-kiosk/src/db/drizzle.ts | 22 +++ .../migration.sql} | 0 .../snapshot.json | 144 ++++++++++++++++++ .../src/db/migrations/meta/0000_snapshot.json | 94 ------------ .../src/db/migrations/meta/_journal.json | 13 -- apps/token-kiosk/src/db/runMigrations.ts | 11 -- apps/token-kiosk/src/runMigrations.mts | 10 ++ apps/token-kiosk/src/users/user.ts | 6 +- apps/token-kiosk/src/users/userManager.ts | 10 +- .../configurations/vitest.globalSetup.mts | 7 +- apps/token-kiosk/tests/files/files.spec.ts | 13 +- apps/token-kiosk/tests/token/token.spec.ts | 13 +- packages/auth-bundler/package.json | 3 +- .../configurations/vitest.globalSetup.mts | 4 +- packages/auth-bundler/tests/db.spec.mts | 13 +- packages/auth-core/dataSource.mjs | 30 ---- packages/auth-core/eslint.config.mjs | 2 +- packages/auth-core/package.json | 5 +- packages/auth-core/src/db/entities/bundle.ts | 53 ------- packages/auth-core/src/db/entities/client.ts | 54 ------- packages/auth-core/src/db/entities/common.ts | 9 -- .../auth-core/src/db/entities/connection.ts | 61 -------- packages/auth-core/src/db/entities/domain.ts | 21 --- packages/auth-core/src/db/entities/key.ts | 38 ----- packages/auth-core/src/db/index.ts | 2 - packages/auth-core/src/db/types/index.ts | 1 - packages/auth-core/src/db/types/interfaces.ts | 8 - .../src/db/utils/createConnection.ts | 59 ------- packages/auth-core/src/db/utils/index.ts | 1 - packages/auth-core/src/drizzle.ts | 16 ++ .../auth-core/src/{db => }/entities/asset.ts | 13 ++ packages/auth-core/src/entities/bundle.ts | 17 +++ packages/auth-core/src/entities/client.ts | 24 +++ .../src/{model => entities}/common.ts | 13 +- packages/auth-core/src/entities/connection.ts | 22 +++ packages/auth-core/src/entities/domain.ts | 9 ++ .../auth-core/src/{db => }/entities/index.ts | 0 packages/auth-core/src/entities/key.ts | 39 +++++ packages/auth-core/src/index.ts | 4 +- .../migration.sql | 0 .../snapshot.json | 0 packages/auth-core/src/model/asset.ts | 41 ----- packages/auth-core/src/model/bundle.ts | 25 --- packages/auth-core/src/model/client.ts | 33 ---- packages/auth-core/src/model/connection.ts | 31 ---- packages/auth-core/src/model/domain.ts | 6 - packages/auth-core/src/model/index.ts | 7 - packages/auth-core/src/model/key.ts | 39 ----- pnpm-lock.yaml | 36 ++++- pnpm-workspace.yaml | 2 +- turbo.json | 1 + 80 files changed, 451 insertions(+), 945 deletions(-) delete mode 100644 apps/auth-manager/src/common/db/constants.ts delete mode 100644 apps/auth-manager/src/common/db/pagination.ts delete mode 100644 apps/auth-manager/src/common/db/sort.ts delete mode 100644 apps/auth-manager/src/common/db/utils.ts delete mode 100644 apps/auth-manager/src/common/errors.ts delete mode 100644 apps/auth-manager/src/common/utils/promiseTimeout.ts create mode 100644 apps/auth-manager/src/runMigrations.mts delete mode 100644 apps/token-kiosk/src/common/utils.ts delete mode 100644 apps/token-kiosk/src/db/createConnection.ts create mode 100644 apps/token-kiosk/src/db/drizzle.ts rename apps/token-kiosk/src/db/migrations/{0000_wise_peter_parker.sql => 20250702132231_wise_peter_parker/migration.sql} (100%) create mode 100644 apps/token-kiosk/src/db/migrations/20250702132231_wise_peter_parker/snapshot.json delete mode 100644 apps/token-kiosk/src/db/migrations/meta/0000_snapshot.json delete mode 100644 apps/token-kiosk/src/db/migrations/meta/_journal.json delete mode 100644 apps/token-kiosk/src/db/runMigrations.ts create mode 100644 apps/token-kiosk/src/runMigrations.mts delete mode 100644 packages/auth-core/dataSource.mjs delete mode 100644 packages/auth-core/src/db/entities/bundle.ts delete mode 100644 packages/auth-core/src/db/entities/client.ts delete mode 100644 packages/auth-core/src/db/entities/common.ts delete mode 100644 packages/auth-core/src/db/entities/connection.ts delete mode 100644 packages/auth-core/src/db/entities/domain.ts delete mode 100644 packages/auth-core/src/db/entities/key.ts delete mode 100644 packages/auth-core/src/db/index.ts delete mode 100644 packages/auth-core/src/db/types/index.ts delete mode 100644 packages/auth-core/src/db/types/interfaces.ts delete mode 100644 packages/auth-core/src/db/utils/createConnection.ts delete mode 100644 packages/auth-core/src/db/utils/index.ts create mode 100644 packages/auth-core/src/drizzle.ts rename packages/auth-core/src/{db => }/entities/asset.ts (68%) create mode 100644 packages/auth-core/src/entities/bundle.ts create mode 100644 packages/auth-core/src/entities/client.ts rename packages/auth-core/src/{model => entities}/common.ts (52%) create mode 100644 packages/auth-core/src/entities/connection.ts create mode 100644 packages/auth-core/src/entities/domain.ts rename packages/auth-core/src/{db => }/entities/index.ts (100%) create mode 100644 packages/auth-core/src/entities/key.ts rename packages/auth-core/src/{db => }/migrations/20260513061159_curious_speedball/migration.sql (100%) rename packages/auth-core/src/{db => }/migrations/20260513061159_curious_speedball/snapshot.json (100%) delete mode 100644 packages/auth-core/src/model/asset.ts delete mode 100644 packages/auth-core/src/model/bundle.ts delete mode 100644 packages/auth-core/src/model/client.ts delete mode 100644 packages/auth-core/src/model/connection.ts delete mode 100644 packages/auth-core/src/model/domain.ts delete mode 100644 packages/auth-core/src/model/index.ts delete mode 100644 packages/auth-core/src/model/key.ts diff --git a/apps/auth-cron/package.json b/apps/auth-cron/package.json index 101853a1..efe7ead5 100644 --- a/apps/auth-cron/package.json +++ b/apps/auth-cron/package.json @@ -40,6 +40,7 @@ "@map-colonies/schemas": "catalog:", "@map-colonies/prometheus": "catalog:", "@map-colonies/tracing": "catalog:", + "@map-colonies/drizzle-utils": "catalog:", "@opentelemetry/api": "catalog:", "croner": "6.0.3", "express": "catalog:", diff --git a/apps/auth-cron/src/index.ts b/apps/auth-cron/src/index.ts index 85739695..ccdbb6e5 100644 --- a/apps/auth-cron/src/index.ts +++ b/apps/auth-cron/src/index.ts @@ -9,7 +9,7 @@ import { createTerminus } from '@godaddy/terminus'; import type { CatchCallbackFn } from 'croner'; import { Cron } from 'croner'; import type { Environments } from '@map-colonies/auth-core'; -import { initConnection } from '@map-colonies/auth-core'; +import { healthCheck, initConnection } from '@map-colonies/drizzle-utils'; import type { commonDbFullV1Type } from '@map-colonies/schemas'; import { BundleDatabase } from '@map-colonies/auth-bundler'; import { collectMetricsExpressMiddleware } from '@map-colonies/prometheus'; @@ -36,7 +36,7 @@ const main = async (): Promise => { const config = getConfig(); const cronConfig = config.get('cron'); const dbConfig = config.get('db'); - const [dataSource, bundleDatabase] = await initDb(dbConfig); + const [pool, bundleDatabase] = await initDb(dbConfig); Object.entries(cronConfig).map(([env, value]) => { logger.info({ msg: 'initializing new update bundle job', bundleEnv: env }); @@ -56,10 +56,13 @@ const main = async (): Promise => { app.use(collectMetricsExpressMiddleware({ registry: metricsRegistry })); const server = createTerminus(createServer(app), { + onSignal: async () => { + logger.info('server is starting cleanup'); + await pool.end(); + logger.info('database connection closed'); + }, healthChecks: { - '/liveness': async () => { - await dataSource.query('SELECT 1'); - }, + '/liveness': healthCheck(pool), }, }); diff --git a/apps/auth-cron/tests/job.spec.mts b/apps/auth-cron/tests/job.spec.mts index 95d4cdd0..5ee74fb0 100644 --- a/apps/auth-cron/tests/job.spec.mts +++ b/apps/auth-cron/tests/job.spec.mts @@ -4,9 +4,7 @@ import path from 'node:path'; import type { BundleDatabase } from '@map-colonies/auth-bundler'; import { createBundle } from '@map-colonies/auth-bundler'; import * as authBundler from '@map-colonies/auth-bundler'; -import type { Bundle } from '@map-colonies/auth-core'; import { Environment } from '@map-colonies/auth-core'; -import type { Repository } from 'typeorm'; import type { Mock } from 'vitest'; import { vi, describe, beforeEach, afterEach, afterAll, it, expect, beforeAll } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; diff --git a/apps/auth-cron/tests/validators.spec.mts b/apps/auth-cron/tests/validators.spec.mts index 339444cb..f59372c2 100644 --- a/apps/auth-cron/tests/validators.spec.mts +++ b/apps/auth-cron/tests/validators.spec.mts @@ -25,7 +25,7 @@ describe('validators.ts', function () { }); it('should throw if the bucket does not exists', async function () { - const promise = validateS3([Environment.PRODUCTION]); + const promise = validateS3([Environment.PROD]); await expect(promise).rejects.toThrow(); }); diff --git a/apps/auth-manager/package.json b/apps/auth-manager/package.json index 4bc76c91..4615cbc5 100644 --- a/apps/auth-manager/package.json +++ b/apps/auth-manager/package.json @@ -47,6 +47,7 @@ "@map-colonies/prometheus": "catalog:", "@map-colonies/tracing": "catalog:", "@map-colonies/tracing-utils": "catalog:", + "@map-colonies/drizzle-utils": "catalog:", "@opentelemetry/api": "catalog:", "auth-openapi": "workspace:^", "body-parser": "catalog:", diff --git a/apps/auth-manager/src/client/controllers/clientController.ts b/apps/auth-manager/src/client/controllers/clientController.ts index 13a6db01..7638a825 100644 --- a/apps/auth-manager/src/client/controllers/clientController.ts +++ b/apps/auth-manager/src/client/controllers/clientController.ts @@ -5,10 +5,9 @@ import { injectable, inject } from 'tsyringe'; import { Client } from '@map-colonies/auth-core'; import { parseISO } from 'date-fns'; import type { TypedRequestHandlers, components, operations } from 'auth-openapi'; +import { DEFAULT_PAGE_SIZE, sortOptionParser } from '@map-colonies/drizzle-utils'; import { SERVICES } from '@common/constants'; -import { DEFAULT_PAGE_SIZE } from '@src/common/db/pagination'; import { removeNulls } from '@src/utils/mapper'; -import { sortOptionParser } from '@src/common/db/sort'; import { ClientManager } from '../models/clientManager'; import { ClientAlreadyExistsError, ClientNotFoundError } from '../models/errors'; import { ClientSearchParams } from '../models/client'; diff --git a/apps/auth-manager/src/client/models/clientManager.ts b/apps/auth-manager/src/client/models/clientManager.ts index 4269e2ab..61610ac9 100644 --- a/apps/auth-manager/src/client/models/clientManager.ts +++ b/apps/auth-manager/src/client/models/clientManager.ts @@ -3,11 +3,16 @@ import { inject, injectable } from 'tsyringe'; import { DatabaseError } from 'pg'; import { count, eq, and, arrayContains, ilike } from 'drizzle-orm'; import { clientTable, type Client, type Drizzle, type NewClient } from '@map-colonies/auth-core'; +import { + createDatesComparison, + isDrizzleQueryError, + type PaginationParams, + paginationParamsToOffsetAndLimit, + pgErrorCodes, + type SortOptions, + sortOptionsToOrderBy, +} from '@map-colonies/drizzle-utils'; import { SERVICES } from '@common/constants'; -import { pgErrorCodes } from '@common/db/constants'; -import { createDatesComparison, isDrizzleQueryError, sortOptionsToOrderBy } from '@common/db/utils'; -import { SortOptions } from '@src/common/db/sort'; -import { PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import { ClientAlreadyExistsError, ClientNotFoundError } from './errors'; import { ClientSearchParams } from './client'; diff --git a/apps/auth-manager/src/common/db/constants.ts b/apps/auth-manager/src/common/db/constants.ts deleted file mode 100644 index ae8ca99b..00000000 --- a/apps/auth-manager/src/common/db/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const pgErrorCodes = { - // eslint-disable-next-line @typescript-eslint/naming-convention - UNIQUE_VIOLATION: '23505', -}; diff --git a/apps/auth-manager/src/common/db/pagination.ts b/apps/auth-manager/src/common/db/pagination.ts deleted file mode 100644 index 0497985e..00000000 --- a/apps/auth-manager/src/common/db/pagination.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { SQL } from 'drizzle-orm'; -import type { PgColumn, PgSelect } from 'drizzle-orm/pg-core'; - -export const DEFAULT_PAGE_SIZE = 10; -export interface PaginationParams { - page: number; - pageSize: number; -} - -export function paginationParamsToOffsetAndLimit(paginationParams?: PaginationParams): { limit: number; offset: number } { - if (paginationParams === undefined) { - return { limit: DEFAULT_PAGE_SIZE, offset: 0 }; - } - - const { page, pageSize } = paginationParams; - - return { - limit: pageSize, - offset: (page - 1) * pageSize, - }; -} diff --git a/apps/auth-manager/src/common/db/sort.ts b/apps/auth-manager/src/common/db/sort.ts deleted file mode 100644 index 51d76d72..00000000 --- a/apps/auth-manager/src/common/db/sort.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SortQueryInvalidFieldError, SortQueryRepeatError } from '../errors'; - -export type SortOptions = { - [key in keyof T]?: 'asc' | 'desc'; -}; - -export function sortOptionParser(sortArray: string[] | undefined, sortFieldsMap: Map): SortOptions { - if (!sortArray) { - return {}; - } - - const parsedOptions: SortOptions = {}; - const fieldSet = new Set(); - - for (const option of sortArray) { - const [field, order] = option.split(':') as [string, 'asc' | 'desc' | undefined]; // we assume that the options are already validated by the openapi validator; - - if (fieldSet.has(field)) { - throw new SortQueryRepeatError(`Duplicate field in sort query: ${field}`); - } - fieldSet.add(field); - - const parsedField = sortFieldsMap.get(field); - - if (parsedField === undefined) { - throw new SortQueryInvalidFieldError(`Invalid field in sort query: ${field}`); - } - - parsedOptions[parsedField] = order ?? 'asc'; - } - - return parsedOptions; -} diff --git a/apps/auth-manager/src/common/db/utils.ts b/apps/auth-manager/src/common/db/utils.ts deleted file mode 100644 index 6b766d5d..00000000 --- a/apps/auth-manager/src/common/db/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { between, lt, gt, type SQL, type Column, asc, type AnyColumn, desc, type Subquery, type DrizzleQueryError } from 'drizzle-orm'; -import type { PgTable } from 'drizzle-orm/pg-core'; -import type { SortOptions } from './sort'; - -export function createDatesComparison(column: Column, earlyDate?: Date, laterDate?: Date): SQL | undefined { - if (earlyDate !== undefined && laterDate !== undefined) { - return between(column, earlyDate, laterDate); - } - if (earlyDate !== undefined) { - return gt(column, earlyDate); - } - if (laterDate !== undefined) { - return lt(column, laterDate); - } - return undefined; -} - -export function sortOptionsToOrderBy(tableDefinition: T, sortOptions: SortOptions): SQL[] { - const result: SQL[] = []; - for (const key in sortOptions) { - const direction = sortOptions[key]; - - if (direction === 'asc') { - result.push(asc(tableDefinition[key] as AnyColumn)); - } else if (direction === 'desc') { - result.push(desc(tableDefinition[key] as AnyColumn)); - } - } - return result; -} - -export function isDrizzleQueryError(err: unknown): err is DrizzleQueryError { - return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'DrizzleQueryError'; -} diff --git a/apps/auth-manager/src/common/errors.ts b/apps/auth-manager/src/common/errors.ts deleted file mode 100644 index 3a0d9414..00000000 --- a/apps/auth-manager/src/common/errors.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class SortQueryRepeatError extends Error { - public constructor(message: string) { - super(message); - Object.setPrototypeOf(this, SortQueryRepeatError.prototype); - } -} - -export class SortQueryInvalidFieldError extends Error { - public constructor(message: string) { - super(message); - Object.setPrototypeOf(this, SortQueryInvalidFieldError.prototype); - } -} diff --git a/apps/auth-manager/src/common/utils/promiseTimeout.ts b/apps/auth-manager/src/common/utils/promiseTimeout.ts deleted file mode 100644 index 1ead48b4..00000000 --- a/apps/auth-manager/src/common/utils/promiseTimeout.ts +++ /dev/null @@ -1,14 +0,0 @@ -class TimeoutError extends Error {} - -export const promiseTimeout = async (ms: number, promise: Promise): Promise => { - // Create a promise that rejects in milliseconds - const timeout = new Promise((_, reject) => { - const id = setTimeout(() => { - clearTimeout(id); - reject(new TimeoutError(`Timed out in + ${ms} + ms.`)); - }, ms); - }); - - // Returns a race between our timeout and the passed in promise - return Promise.race([promise, timeout]); -}; diff --git a/apps/auth-manager/src/connection/controllers/connectionController.ts b/apps/auth-manager/src/connection/controllers/connectionController.ts index 55083405..1cb06840 100644 --- a/apps/auth-manager/src/connection/controllers/connectionController.ts +++ b/apps/auth-manager/src/connection/controllers/connectionController.ts @@ -2,13 +2,12 @@ import { HttpError } from '@map-colonies/error-express-handler'; import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import { type Logger } from '@map-colonies/js-logger'; +import { DEFAULT_PAGE_SIZE, sortOptionParser } from '@map-colonies/drizzle-utils'; import { Connection } from '@map-colonies/auth-core'; import type { TypedRequestHandlers, components } from 'auth-openapi'; import { SERVICES } from '@common/constants'; import { ClientNotFoundError } from '@client/models/errors'; -import { DEFAULT_PAGE_SIZE } from '@src/common/db/pagination'; import { DomainNotFoundError } from '@domain/models/errors'; -import { sortOptionParser } from '@src/common/db/sort'; import { KeyNotFoundError } from '@key/models/errors'; import { ConnectionManager } from '../models/connectionManager'; import { ConnectionNotFoundError, ConnectionVersionMismatchError } from '../models/errors'; diff --git a/apps/auth-manager/src/connection/models/connectionManager.ts b/apps/auth-manager/src/connection/models/connectionManager.ts index ce214adb..15328606 100644 --- a/apps/auth-manager/src/connection/models/connectionManager.ts +++ b/apps/auth-manager/src/connection/models/connectionManager.ts @@ -4,16 +4,15 @@ import { inject, injectable } from 'tsyringe'; import { JWK } from 'jose'; import { paths } from 'auth-openapi'; import { ilike, SQL, inArray, eq, arrayContains, count, and, desc, countDistinct, sql } from 'drizzle-orm'; +import { sortOptionsToOrderBy } from '@map-colonies/drizzle-utils'; +import { type PaginationParams, type SortOptions, paginationParamsToOffsetAndLimit } from '@map-colonies/drizzle-utils'; import { ClientNotFoundError } from '@client/models/errors'; -import { sortOptionsToOrderBy } from '@src/common/db/utils'; import { SERVICES } from '@common/constants'; import { DomainRepository } from '@domain/DAL/domainRepository'; import { DomainNotFoundError } from '@domain/models/errors'; import { KeyRepository } from '@key/DAL/keyRepository'; import { generateToken } from '@common/crypto'; -import { PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; import { KeyNotFoundError } from '@key/models/errors'; -import { SortOptions } from '@src/common/db/sort'; import { asteriskStringComparatorLast } from '@src/utils/utils'; import { ConnectionRepository } from '../DAL/connectionRepository'; import { ConnectionVersionMismatchError, ConnectionNotFoundError } from './errors'; diff --git a/apps/auth-manager/src/containerConfig.ts b/apps/auth-manager/src/containerConfig.ts index b9908b79..a482a7c6 100644 --- a/apps/auth-manager/src/containerConfig.ts +++ b/apps/auth-manager/src/containerConfig.ts @@ -3,15 +3,14 @@ import { trace } from '@opentelemetry/api'; import { instanceCachingFactory } from 'tsyringe'; import type { DependencyContainer } from 'tsyringe/dist/typings/types'; import { jsLogger } from '@map-colonies/js-logger'; -import type { HealthCheck } from '@godaddy/terminus'; import { Pool } from 'pg'; -import { createDrizzle, initConnection } from '@map-colonies/auth-core'; +import { createDrizzle } from '@map-colonies/auth-core'; +import { healthCheck, initConnection } from '@map-colonies/drizzle-utils'; import { Registry } from 'prom-client'; -import { DB_CONNECTION_TIMEOUT, SERVICES, SERVICE_NAME } from './common/constants'; +import { SERVICES, SERVICE_NAME } from './common/constants'; import { domainRouterFactory, DOMAIN_ROUTER_SYMBOL } from './domain/routes/domainRouter'; import type { InjectionObject } from './common/dependencyRegistration'; import { registerDependencies } from './common/dependencyRegistration'; -import { promiseTimeout } from './common/utils/promiseTimeout'; import { clientRouterFactory, CLIENT_ROUTER_SYMBOL } from './client/routes/clientRouter'; import { keyRouterFactory, KEY_ROUTER_SYMBOL } from './key/routes/keyRouter'; import { assetRouterFactory, ASSET_ROUTER_SYMBOL } from './asset/routes/assetRouter'; @@ -20,15 +19,6 @@ import { bundleRouterFactory, BUNDLE_ROUTER_SYMBOL } from './bundle/routes/bundl import { getConfig } from './common/config'; import { getTracing } from './common/tracing'; -const healthCheck = (connection: Pool): HealthCheck => { - return async (): Promise => { - const check = connection.query('SELECT 1').then(() => { - return; - }); - return promiseTimeout(DB_CONNECTION_TIMEOUT, check); - }; -}; - export interface RegisterOptions { override?: InjectionObject[]; useChild?: boolean; @@ -42,7 +32,6 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise const logger = await jsLogger({ ...loggerConfig, prettyPrint: loggerConfig.prettyPrint, mixin: getOtelMixin() }); const dataSourceOptions = configInstance.get('db'); - // const connection = await initConnection(dataSourceOptions); let pool: Pool; try { diff --git a/apps/auth-manager/src/domain/controllers/domainController.ts b/apps/auth-manager/src/domain/controllers/domainController.ts index 3e4d658d..f74b96be 100644 --- a/apps/auth-manager/src/domain/controllers/domainController.ts +++ b/apps/auth-manager/src/domain/controllers/domainController.ts @@ -3,8 +3,7 @@ import httpStatus from 'http-status-codes'; import { injectable, inject } from 'tsyringe'; import type { Domain } from '@map-colonies/auth-core'; import type { TypedRequestHandlers } from 'auth-openapi'; -import { sortOptionParser } from '@src/common/db/sort'; -import { DEFAULT_PAGE_SIZE } from '@src/common/db/pagination'; +import { DEFAULT_PAGE_SIZE, sortOptionParser } from '@map-colonies/drizzle-utils'; import { DomainManager } from '../models/domainManager'; import { DomainAlreadyExistsError } from '../models/errors'; diff --git a/apps/auth-manager/src/domain/models/domainManager.ts b/apps/auth-manager/src/domain/models/domainManager.ts index 24b46174..bc505741 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -2,9 +2,8 @@ import type { Logger } from '@map-colonies/js-logger'; import { Domain, domainTable, type NewDomain, type Drizzle } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; import { count } from 'drizzle-orm'; -import { type PaginationParams, paginationParamsToOffsetAndLimit } from '@src/common/db/pagination'; -import type { SortOptions } from '@src/common/db/sort'; -import { isDrizzleQueryError } from '@src/common/db/utils'; +import { isDrizzleQueryError } from '@map-colonies/drizzle-utils'; +import { type PaginationParams, paginationParamsToOffsetAndLimit, type SortOptions } from '@map-colonies/drizzle-utils'; import { SERVICES } from '@common/constants'; import { DomainAlreadyExistsError } from './errors'; diff --git a/apps/auth-manager/src/runMigrations.mts b/apps/auth-manager/src/runMigrations.mts new file mode 100644 index 00000000..39aa0853 --- /dev/null +++ b/apps/auth-manager/src/runMigrations.mts @@ -0,0 +1,10 @@ +import { initConnection } from '@map-colonies/drizzle-utils'; +import { createDrizzle, runMigrations } from '@map-colonies/auth-core'; +import { getConfig, initConfig } from './common/config.js'; + +await initConfig(); +const config = getConfig(); +const pool = await initConnection(config.get('db')); +await runMigrations(createDrizzle(pool)); +await pool.end(); +console.log('Migrations completed'); diff --git a/apps/auth-manager/tests/configurations/vitest.globalSetup.mts b/apps/auth-manager/tests/configurations/vitest.globalSetup.mts index e5b200b2..8fcc13b0 100644 --- a/apps/auth-manager/tests/configurations/vitest.globalSetup.mts +++ b/apps/auth-manager/tests/configurations/vitest.globalSetup.mts @@ -1,6 +1,6 @@ import 'reflect-metadata'; import path from 'node:path'; -import { initConnection } from '@map-colonies/auth-core'; +import { initConnection } from '@map-colonies/drizzle-utils'; import { createPostgresContainer, resetAndMigrate, mergeTestConfig, PG_PORT } from 'test-utils'; import { getConfig, initConfig } from '@src/common/config.js'; diff --git a/apps/auth-manager/tests/integration/asset/asset.spec.mts b/apps/auth-manager/tests/integration/asset/asset.spec.mts index fbd5489c..c48ff2bd 100644 --- a/apps/auth-manager/tests/integration/asset/asset.spec.mts +++ b/apps/auth-manager/tests/integration/asset/asset.spec.mts @@ -23,10 +23,6 @@ describe('client', function () { drizzle = env.drizzle; }); - // afterAll(async function () { - // await depContainer.resolve(Pool).end(); - // }); - describe('Happy Path', function () { describe('GET /assets', function () { it('should return 200 status code and all the assets', async function () { diff --git a/apps/auth-manager/tests/integration/connection/connection.spec.mts b/apps/auth-manager/tests/integration/connection/connection.spec.mts index 911d2dbb..f675c69e 100644 --- a/apps/auth-manager/tests/integration/connection/connection.spec.mts +++ b/apps/auth-manager/tests/integration/connection/connection.spec.mts @@ -250,8 +250,8 @@ describe('connection', function () { // connection.environment = Environment.STAGE; // await depContainer.resolve(DataSource).getRepository(Client).save(client); // await depContainer.resolve(DataSource).getRepository(Connection).save(connection); - await drizzle.insert(clientTable).values(client); - await drizzle.insert(connectionTable).values(connection); + await drizzle.insert(clientTable).values(client).onConflictDoNothing(); + await drizzle.insert(connectionTable).values(connection).onConflictDoNothing(); // const keyRepo = depContainer.resolve(DataSource).getRepository(Key); // await keyRepo.clear(); // await keyRepo.save({ environment: connection.environment, version: 1, privateKey: keys[0], publicKey: keys[1] }); diff --git a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts index 313f54aa..08bcfcab 100644 --- a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts +++ b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts @@ -2,10 +2,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { jsLogger } from '@map-colonies/js-logger'; import { DatabaseError } from 'pg'; import { getFakeClient } from 'test-utils'; +import { pgErrorCodes } from '@map-colonies/drizzle-utils'; import type { Drizzle } from '@map-colonies/auth-core'; import { ClientManager } from '@src/client/models/clientManager.js'; import { ClientAlreadyExistsError } from '@src/client/models/errors.js'; -import { pgErrorCodes } from '@src/common/db/constants.js'; const logger = await jsLogger({ enabled: false }); diff --git a/apps/token-kiosk/drizzle.config.mts b/apps/token-kiosk/drizzle.config.mts index 4a18c3ad..dd66f8dc 100644 --- a/apps/token-kiosk/drizzle.config.mts +++ b/apps/token-kiosk/drizzle.config.mts @@ -1,8 +1,8 @@ import { ConnectionConfig } from 'pg'; import type { Config as DrizzleConfig } from 'drizzle-kit'; +import { createConnectionOptions } from '@map-colonies/drizzle-utils'; import config from 'config'; -import { createConnectionOptions } from './src/db/createConnection'; import { ConnectionOptions } from 'node:tls'; const dbOptions = createConnectionOptions(config.get('db')) as Omit, 'password' | 'ssl'> & { diff --git a/apps/token-kiosk/package.json b/apps/token-kiosk/package.json index eff1e4e2..1d3493a2 100644 --- a/apps/token-kiosk/package.json +++ b/apps/token-kiosk/package.json @@ -14,10 +14,10 @@ "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run assets:copy", "start": "npm run build && cd dist && node --import ./instrumentation.mjs ./index.js", "start:dev": "npm run build && cd dist && cross-env CONFIG_OFFLINE_MODE=true node --enable-source-maps --import ./instrumentation.mjs ./index.js", - "assets:copy": "copyfiles -f ./config/* ./dist/config && copyfiles -f ./openapi3.yaml ./dist/ && copyfiles ./package.json dist && copyfiles -u 1 ./src/db/migrations/* ./dist/ && copyfiles -u 1 ./src/db/migrations/meta/* ./dist/", + "assets:copy": "copyfiles -f ./config/* ./dist/config && copyfiles -f ./openapi3.yaml ./dist/ && copyfiles ./package.json dist && copyfiles -u 1 ./src/db/migrations/**/* ./dist/", "clean": "rimraf dist", "migration:create": "drizzle-kit generate --config drizzle.config.mts", - "migration:run": "ts-node ./src/db/runMigrations.ts", + "migration:run": "node ./dist/db/runMigrations.mjs", "build:docker": "docker buildx build --build-arg APP_NAME=$npm_package_name -f ../../docker/backend.Dockerfile -t ${DOCKER_REGISTRY:-}${npm_package_name}:${DOCKER_TAG:-latest} ${DOCKER_FLAGS:-} ../..", "build:docker-no-cache": "cross-env DOCKER_FLAGS=--no-cache pnpm run build:docker", "knip": "knip --directory ../.. --workspace apps/token-kiosk" @@ -39,6 +39,7 @@ "@map-colonies/prometheus": "catalog:", "@map-colonies/tracing": "catalog:", "@map-colonies/tracing-utils": "catalog:", + "@map-colonies/drizzle-utils": "catalog:", "@opentelemetry/api": "catalog:", "async-cache-dedupe": "^2.2.0", "compression": "catalog:", @@ -87,7 +88,6 @@ "tsc-alias": "catalog:", "typescript": "catalog:", "vitest": "catalog:", - "ts-node": "^10.9.2", "@map-colonies/vitest-utils": "catalog:" } } diff --git a/apps/token-kiosk/src/common/constants.ts b/apps/token-kiosk/src/common/constants.ts index d6dce2a9..cf35d000 100644 --- a/apps/token-kiosk/src/common/constants.ts +++ b/apps/token-kiosk/src/common/constants.ts @@ -2,8 +2,6 @@ import { readPackageJsonSync } from '@map-colonies/read-pkg'; export const SERVICE_NAME = readPackageJsonSync().name ?? 'unknown_service'; -export const DB_CONNECTION_TIMEOUT = 5000; - export const IGNORED_OUTGOING_TRACE_ROUTES = [/^.*\/v1\/metrics.*$/]; export const IGNORED_INCOMING_TRACE_ROUTES = [/^.*\/docs.*$/]; diff --git a/apps/token-kiosk/src/common/utils.ts b/apps/token-kiosk/src/common/utils.ts deleted file mode 100644 index 6c5422c5..00000000 --- a/apps/token-kiosk/src/common/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -class TimeoutError extends Error {} - -export async function promiseTimeout(ms: number, promise: Promise): Promise { - // Create a promise that rejects in milliseconds - const timeout = new Promise((_, reject) => { - const id = setTimeout(() => { - clearTimeout(id); - reject(new TimeoutError(`Timed out in + ${ms} + ms.`)); - }, ms); - }); - - // Returns a race between our timeout and the passed in promise - return Promise.race([promise, timeout]); -} diff --git a/apps/token-kiosk/src/containerConfig.ts b/apps/token-kiosk/src/containerConfig.ts index 6a663602..146b6b65 100644 --- a/apps/token-kiosk/src/containerConfig.ts +++ b/apps/token-kiosk/src/containerConfig.ts @@ -5,6 +5,7 @@ import type { DependencyContainer } from 'tsyringe/dist/typings/types'; import { jsLogger } from '@map-colonies/js-logger'; import type { Pool } from 'pg'; import { instanceCachingFactory, instancePerContainerCachingFactory } from 'tsyringe'; +import { healthCheck, initConnection } from '@map-colonies/drizzle-utils'; import { registerDependencies, type InjectionObject } from '@common/dependencyRegistration'; import { SERVICES, SERVICE_NAME } from '@common/constants'; import { getTracing } from '@common/tracing'; @@ -12,10 +13,10 @@ import { tokenRouterFactory, TOKEN_ROUTER_SYMBOL } from './tokens/routes/tokenRo import { getConfig } from './common/config'; import { AUTH_ROUTER_SYMBOL, authRouterFactory } from './auth/routes/authRouter'; import { authManagerClientFactory } from './tokens/models/authManagerClient'; -import { createConnectionOptions, createDrizzle, healthCheck, initConnection } from './db/createConnection'; import { openidAuthMiddlewareFactory } from './auth/middlewares/openid'; import { GUIDES_ROUTER_SYMBOL, guidesRouterFactory } from './guides/routes/guidesRouter'; import { FILES_ROUTER_SYMBOL, filesRouterFactory } from './files/routes/filesRouter'; +import { createDrizzle } from './db/drizzle'; export interface RegisterOptions { override?: InjectionObject[]; @@ -40,7 +41,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise let pool: Pool; try { - pool = await initConnection(createConnectionOptions(configInstance.get('db'))); + pool = await initConnection(configInstance.get('db')); } catch (error) { throw new Error(`Failed to connect to the database`, { cause: error }); } diff --git a/apps/token-kiosk/src/db/createConnection.ts b/apps/token-kiosk/src/db/createConnection.ts deleted file mode 100644 index 6d78b956..00000000 --- a/apps/token-kiosk/src/db/createConnection.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { hostname } from 'node:os'; -import { readFileSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; -import type { commonDbFullV1Type } from '@map-colonies/schemas'; -import { drizzle } from 'drizzle-orm/node-postgres'; -import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import { Pool, type PoolConfig } from 'pg'; -import type { HealthCheck } from '@godaddy/terminus'; -import { promiseTimeout } from '../common/utils'; -import { DB_CONNECTION_TIMEOUT } from '../common/constants'; -import { users } from '../users/user'; - -export type DbConfig = PoolConfig & commonDbFullV1Type; - -export function createConnectionOptions(dbConfig: DbConfig): PoolConfig { - const { ssl, ...dataSourceOptions } = dbConfig; - dataSourceOptions.application_name = `${hostname()}-${process.env.NODE_ENV ?? 'unknown_env'}`; - - const poolConfig: PoolConfig = { - ...dataSourceOptions, - user: dbConfig.username, - }; - if (ssl.enabled) { - delete poolConfig.password; - poolConfig.ssl = { key: readFileSync(ssl.key), cert: readFileSync(ssl.cert), ca: readFileSync(ssl.ca) }; - } - return poolConfig; -} - -export async function initConnection(dbConfig: PoolConfig): Promise { - const pool = new Pool(dbConfig); - await pool.query('SELECT NOW()'); - return pool; -} - -export type Drizzle = ReturnType; - -export function createDrizzle(pool: Pool): ReturnType> { - return drizzle(pool, { - schema: { - users, - }, - }); -} - -export function healthCheck(connection: Pool): HealthCheck { - return async (): Promise => { - const check = connection.query('SELECT 1').then(() => { - return; - }); - return promiseTimeout(DB_CONNECTION_TIMEOUT, check); - }; -} - -// maybe we should test migrations as well. for now, we'll just ignore them -/* istanbul ignore next */ -export async function runMigrations(drizzle: Drizzle): Promise { - const optionalFolders = ['./src/db/migrations', './db/migrations', './migrations']; - let migrationsFolder: string | null = null; - - for (const folder of optionalFolders) { - if (existsSync(join(folder, '/meta/_journal.json'))) { - migrationsFolder = folder; - break; - } - } - - if (migrationsFolder === null) { - throw new Error('No migrations folder found'); - } - - await migrate(drizzle, { migrationsFolder: migrationsFolder, migrationsSchema: 'token_kiosk' }); -} diff --git a/apps/token-kiosk/src/db/drizzle.ts b/apps/token-kiosk/src/db/drizzle.ts new file mode 100644 index 00000000..a8508d26 --- /dev/null +++ b/apps/token-kiosk/src/db/drizzle.ts @@ -0,0 +1,22 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import type { Pool } from 'pg'; +import { runMigrations as runMigrationsBase } from '@map-colonies/drizzle-utils'; +import { defineRelations } from 'drizzle-orm'; + +import { usersTable } from '../users/user'; + +const relations = defineRelations({ + users: usersTable, +}); + +export type Drizzle = ReturnType>; + +export function createDrizzle(pool: Pool): Drizzle { + return drizzle({ client: pool, relations }); +} + +// maybe we should test migrations as well. for now, we'll just ignore them +/* istanbul ignore next */ +export async function runMigrations(drizzle: Drizzle): Promise { + await runMigrationsBase(drizzle, 'token_kiosk', __dirname); +} diff --git a/apps/token-kiosk/src/db/migrations/0000_wise_peter_parker.sql b/apps/token-kiosk/src/db/migrations/20250702132231_wise_peter_parker/migration.sql similarity index 100% rename from apps/token-kiosk/src/db/migrations/0000_wise_peter_parker.sql rename to apps/token-kiosk/src/db/migrations/20250702132231_wise_peter_parker/migration.sql diff --git a/apps/token-kiosk/src/db/migrations/20250702132231_wise_peter_parker/snapshot.json b/apps/token-kiosk/src/db/migrations/20250702132231_wise_peter_parker/snapshot.json new file mode 100644 index 00000000..2b065069 --- /dev/null +++ b/apps/token-kiosk/src/db/migrations/20250702132231_wise_peter_parker/snapshot.json @@ -0,0 +1,144 @@ +{ + "id": "750b2c89-56db-4b11-a3b0-0744009a3635", + "prevIds": ["00000000-0000-0000-0000-000000000000"], + "version": "8", + "dialect": "postgres", + "ddl": [ + { + "name": "token_kiosk", + "entityType": "schemas" + }, + { + "isRlsEnabled": false, + "name": "users", + "schema": "token_kiosk", + "entityType": "tables" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "users_pkey", + "schema": "token_kiosk", + "table": "users", + "entityType": "pks" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'{}'", + "generated": null, + "identity": null, + "name": "metadata", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "last_requested_at", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "token_creation_date", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "timestamp with time zone", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_expiration_date", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "token_creation_count", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "is_banned", + "schema": "token_kiosk", + "table": "users", + "entityType": "columns" + } + ], + "renames": [] +} diff --git a/apps/token-kiosk/src/db/migrations/meta/0000_snapshot.json b/apps/token-kiosk/src/db/migrations/meta/0000_snapshot.json deleted file mode 100644 index fb6a9b13..00000000 --- a/apps/token-kiosk/src/db/migrations/meta/0000_snapshot.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "id": "750b2c89-56db-4b11-a3b0-0744009a3635", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "token_kiosk.users": { - "name": "users", - "schema": "token_kiosk", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_requested_at": { - "name": "last_requested_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token_creation_date": { - "name": "token_creation_date", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "token_expiration_date": { - "name": "token_expiration_date", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token_creation_count": { - "name": "token_creation_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "is_banned": { - "name": "is_banned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": { - "token_kiosk": "token_kiosk" - }, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/apps/token-kiosk/src/db/migrations/meta/_journal.json b/apps/token-kiosk/src/db/migrations/meta/_journal.json deleted file mode 100644 index 72276d8f..00000000 --- a/apps/token-kiosk/src/db/migrations/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1751462551655, - "tag": "0000_wise_peter_parker", - "breakpoints": true - } - ] -} diff --git a/apps/token-kiosk/src/db/runMigrations.ts b/apps/token-kiosk/src/db/runMigrations.ts deleted file mode 100644 index ee6b2995..00000000 --- a/apps/token-kiosk/src/db/runMigrations.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { getConfig, initConfig } from '../common/config'; -import { createConnectionOptions, initConnection, createDrizzle, runMigrations } from './createConnection'; - -(async (): Promise => { - await initConfig(); - const config = getConfig(); - const pool = await initConnection(createConnectionOptions(config.get('db'))); - await runMigrations(createDrizzle(pool)); - await pool.end(); - console.log('Migrations completed'); -})().catch(console.error); diff --git a/apps/token-kiosk/src/runMigrations.mts b/apps/token-kiosk/src/runMigrations.mts new file mode 100644 index 00000000..2c3097ac --- /dev/null +++ b/apps/token-kiosk/src/runMigrations.mts @@ -0,0 +1,10 @@ +import { initConnection } from '@map-colonies/drizzle-utils'; +import { getConfig, initConfig } from './common/config.js'; +import { createDrizzle, runMigrations } from './db/drizzle.js'; + +await initConfig(); +const config = getConfig(); +const pool = await initConnection(config.get('db')); +await runMigrations(createDrizzle(pool)); +await pool.end(); +console.log('Migrations completed'); diff --git a/apps/token-kiosk/src/users/user.ts b/apps/token-kiosk/src/users/user.ts index 472da074..abcedc15 100644 --- a/apps/token-kiosk/src/users/user.ts +++ b/apps/token-kiosk/src/users/user.ts @@ -2,7 +2,7 @@ import { integer, jsonb, pgSchema, text, timestamp, boolean } from 'drizzle-orm/ const pgDbSchema = pgSchema('token_kiosk'); -export const users = pgDbSchema.table('users', { +export const usersTable = pgDbSchema.table('users', { id: text('id').primaryKey().notNull(), metadata: jsonb('metadata').notNull().default({}), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), @@ -14,5 +14,5 @@ export const users = pgDbSchema.table('users', { isBanned: boolean('is_banned').notNull().default(false), }); -export type User = typeof users.$inferSelect; -export type UserInsert = typeof users.$inferInsert; +export type User = typeof usersTable.$inferSelect; +export type UserInsert = typeof usersTable.$inferInsert; diff --git a/apps/token-kiosk/src/users/userManager.ts b/apps/token-kiosk/src/users/userManager.ts index 44ebc284..987528b9 100644 --- a/apps/token-kiosk/src/users/userManager.ts +++ b/apps/token-kiosk/src/users/userManager.ts @@ -1,8 +1,8 @@ import { inject, injectable } from 'tsyringe'; import { eq } from 'drizzle-orm'; import { SERVICES } from '@src/common/constants'; -import { type Drizzle } from '@src/db/createConnection'; -import { User, UserInsert, users } from './user'; +import { type Drizzle } from '@src/db/drizzle'; +import { User, UserInsert, usersTable } from './user'; type CreateUser = Omit; type UpdateUser = Partial>; @@ -12,13 +12,13 @@ export class UserManager { public constructor(@inject(SERVICES.DRIZZLE) private readonly drizzle: Drizzle) {} public async getUserById(userId: string): Promise { - const result = await this.drizzle.query.users.findFirst({ where: eq(users.id, userId) }); + const result = await this.drizzle.query.users.findFirst({ where: { id: userId } }); return result; } public async createUser(user: CreateUser): Promise { const result = await this.drizzle - .insert(users) + .insert(usersTable) .values({ ...user, tokenCreationCount: 1 }) .returning(); if (result.length === 0 || !result[0]) { @@ -28,7 +28,7 @@ export class UserManager { } public async updateUser(userId: string, userData: UpdateUser): Promise { - const result = await this.drizzle.update(users).set(userData).where(eq(users.id, userId)).returning(); + const result = await this.drizzle.update(usersTable).set(userData).where(eq(usersTable.id, userId)).returning(); if (result.length === 0 || !result[0]) { throw new Error('Failed to update user'); } diff --git a/apps/token-kiosk/tests/configurations/vitest.globalSetup.mts b/apps/token-kiosk/tests/configurations/vitest.globalSetup.mts index c4cd9995..8dfc656e 100644 --- a/apps/token-kiosk/tests/configurations/vitest.globalSetup.mts +++ b/apps/token-kiosk/tests/configurations/vitest.globalSetup.mts @@ -1,6 +1,7 @@ import path from 'node:path'; import { createPostgresContainer, mergeTestConfig, PG_PORT } from 'test-utils'; -import { initConnection, createConnectionOptions, createDrizzle, runMigrations } from '@src/db/createConnection.js'; +import { initConnection } from '@map-colonies/drizzle-utils'; +import { createDrizzle, runMigrations } from '@src/db/drizzle.js'; import { getConfig, initConfig } from '@src/common/config.js'; export async function setup(): Promise { @@ -16,7 +17,7 @@ export async function setup(): Promise { const port = container.getMappedPort(PG_PORT); await mergeTestConfig(path.join(__dirname, '../../config'), { 'db.port': port }); - const pool = await initConnection(createConnectionOptions({ ...config, port })); + const pool = await initConnection({ ...config, port }); const drizzle = createDrizzle(pool); await runMigrations(drizzle); await pool.end(); @@ -26,7 +27,7 @@ export async function teardown(): Promise { await initConfig(true); const config = getConfig().get('db'); - const pool = await initConnection(createConnectionOptions(config)); + const pool = await initConnection(config); await pool.query('DROP SCHEMA IF EXISTS token_kiosk CASCADE'); await pool.end(); } diff --git a/apps/token-kiosk/tests/files/files.spec.ts b/apps/token-kiosk/tests/files/files.spec.ts index a5e0df4a..9643d821 100644 --- a/apps/token-kiosk/tests/files/files.spec.ts +++ b/apps/token-kiosk/tests/files/files.spec.ts @@ -10,8 +10,8 @@ import type { RequestContext } from 'express-openid-connect'; import type { RequestHandler } from 'express'; import type { paths, operations } from 'token-openapi'; import { getApp } from '@src/app'; -import type { Drizzle } from '@src/db/createConnection'; -import { users } from '@src/users/user'; +import type { Drizzle } from '@src/db/drizzle'; +import { usersTable } from '@src/users/user'; import { SERVICES } from '@common/constants'; import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; import { initConfig } from '@src/common/config'; @@ -53,9 +53,9 @@ describe('guides', function () { nock('http://localhost:8082').get('/key/prod/latest').reply(httpStatusCodes.OK, privateKey); await drizzle - .insert(users) + .insert(usersTable) .values({ id: 'files@example.com', token: 'aaaaaaa', isBanned: false, tokenExpirationDate: addWeeks(new Date(), 1) }) - .onConflictDoUpdate({ set: { token: 'aaaaaaa' }, target: users.id }); + .onConflictDoUpdate({ set: { token: 'aaaaaaa' }, target: usersTable.id }); }); afterEach(function () { @@ -107,7 +107,10 @@ describe('guides', function () { }); it('should return 403 status code when user is banned', async function () { - await drizzle.insert(users).values({ id: 'xd@gmail.com', token: '', isBanned: true, tokenExpirationDate: new Date() }).onConflictDoNothing(); + await drizzle + .insert(usersTable) + .values({ id: 'xd@gmail.com', token: '', isBanned: true, tokenExpirationDate: new Date() }) + .onConflictDoNothing(); // Mock user as banned by setting up the context to simulate a banned user const bannedUserContext = { diff --git a/apps/token-kiosk/tests/token/token.spec.ts b/apps/token-kiosk/tests/token/token.spec.ts index fb72aec3..b3b4dae4 100644 --- a/apps/token-kiosk/tests/token/token.spec.ts +++ b/apps/token-kiosk/tests/token/token.spec.ts @@ -12,8 +12,8 @@ import { eq } from 'drizzle-orm'; import { subWeeks } from 'date-fns'; import type { paths, operations } from 'token-openapi'; import { getApp } from '@src/app'; -import type { Drizzle } from '@src/db/createConnection'; -import { users } from '@src/users/user'; +import type { Drizzle } from '@src/db/drizzle'; +import { usersTable } from '@src/users/user'; import { SERVICES } from '@common/constants'; import { initConfig } from '@src/common/config'; import { OPENAPI_PATH } from '@tests/utils/paths.mjs'; @@ -131,11 +131,11 @@ describe('token', function () { expect(firstRes).toHaveProperty('statusCode', httpStatusCodes.OK); await drizzle - .update(users) + .update(usersTable) .set({ tokenExpirationDate: subWeeks(new Date(), 5), // Set expiration to the past }) - .where(eq(users.id, email)) + .where(eq(usersTable.id, email)) .execute(); await sleep(1000); // Wait for a short period to ensure the token is considered expired @@ -153,7 +153,10 @@ describe('token', function () { describe('Bad Path', function () { it('should return 403 status code when user is banned', async function () { - await drizzle.insert(users).values({ id: 'xd@gmail.com', token: '', isBanned: true, tokenExpirationDate: new Date() }).onConflictDoNothing(); + await drizzle + .insert(usersTable) + .values({ id: 'xd@gmail.com', token: '', isBanned: true, tokenExpirationDate: new Date() }) + .onConflictDoNothing(); // Mock user as banned by setting up the context to simulate a banned user const bannedUserContext = { diff --git a/packages/auth-bundler/package.json b/packages/auth-bundler/package.json index 2f1f7460..8fdff5d0 100644 --- a/packages/auth-bundler/package.json +++ b/packages/auth-bundler/package.json @@ -42,7 +42,8 @@ "execa": "^7.1.1", "handlebars": "4.7.7", "pg": "catalog:", - "drizzle-orm": "catalog:" + "drizzle-orm": "catalog:", + "@map-colonies/drizzle-utils": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", diff --git a/packages/auth-bundler/tests/configurations/vitest.globalSetup.mts b/packages/auth-bundler/tests/configurations/vitest.globalSetup.mts index 1f7e9d0d..242710d3 100644 --- a/packages/auth-bundler/tests/configurations/vitest.globalSetup.mts +++ b/packages/auth-bundler/tests/configurations/vitest.globalSetup.mts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { initConnection } from '@map-colonies/auth-core'; +import { initConnection } from '@map-colonies/drizzle-utils'; import type { TestProject } from 'vitest/node'; import { createPostgresContainer, PG_PORT, createAndProvideTempDir, removeTempDir, resetAndMigrate, mergeTestConfig } from 'test-utils'; import { getConfig, initConfig } from '../helpers/config.js'; @@ -22,7 +22,7 @@ export async function setup(project: TestProject): Promise { const connection = await initConnection({ ...dataSourceOptions, port }); - await resetAndMigrate(connection, dataSourceOptions.schema); + await resetAndMigrate(connection); } export async function teardown(): Promise { diff --git a/packages/auth-bundler/tests/db.spec.mts b/packages/auth-bundler/tests/db.spec.mts index bcb93ef8..cafb6a18 100644 --- a/packages/auth-bundler/tests/db.spec.mts +++ b/packages/auth-bundler/tests/db.spec.mts @@ -1,17 +1,8 @@ /// -import { - assetTable, - Environment, - initConnection, - createDrizzle, - type Drizzle, - keyTable, - connectionTable, - bundleTable, - type Asset, -} from '@map-colonies/auth-core'; +import { assetTable, Environment, createDrizzle, type Drizzle, keyTable, connectionTable, bundleTable, type Asset } from '@map-colonies/auth-core'; import type { Pool } from 'pg'; +import { initConnection } from '@map-colonies/drizzle-utils'; import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest'; import { getFakeAsset, getFakeConnection, getMockKeys } from 'test-utils'; import { BundleDatabase } from '@src/db.js'; diff --git a/packages/auth-core/dataSource.mjs b/packages/auth-core/dataSource.mjs deleted file mode 100644 index f41f8ceb..00000000 --- a/packages/auth-core/dataSource.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import { DataSource } from 'typeorm'; - -/** - * - * @param {string} moduleName - * @returns {Promise} - */ -async function importModule(moduleName) { - let imported; - try { - imported = await import(`./${moduleName}`); - } catch (err) { - if (err instanceof Error && 'code' in err && err.code === 'ERR_MODULE_NOT_FOUND') { - imported = await import(`./dist/${moduleName}`); - } else { - throw err; - } - } - return imported; -} - -const { getConfig, initConfig } = await importModule('config.js'); -await initConfig(); -const connectionOptions = getConfig().getAll(); -const { createConnectionOptions } = await importModule('db/utils/createConnection.js'); -const appDataSource = new DataSource({ - ...createConnectionOptions(connectionOptions), -}); - -export default appDataSource; diff --git a/packages/auth-core/eslint.config.mjs b/packages/auth-core/eslint.config.mjs index 5338ece2..c60ff3a0 100644 --- a/packages/auth-core/eslint.config.mjs +++ b/packages/auth-core/eslint.config.mjs @@ -1,4 +1,4 @@ import tsBaseConfig from '@map-colonies/eslint-config/ts-base'; import { defineConfig, globalIgnores } from 'eslint/config'; -export default defineConfig(tsBaseConfig, globalIgnores(['drizzle.config.ts', 'vitest.config.mts', '**/migrations/**'])); +export default defineConfig(tsBaseConfig, globalIgnores(['drizzle.config.mts', 'vitest.config.mts', '**/migrations/**'])); diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index 8e5cd986..5f0b8789 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -28,7 +28,7 @@ "lint:fix": "eslint --fix .", "prebuild": "pnpm run clean", "build": "tsc --project tsconfig.json && pnpm run assets:copy", - "assets:copy": "copyfiles -u 3 src/db/migrations/**/* dist/db/migrations", + "assets:copy": "copyfiles -u 2 src/migrations/**/* dist/migrations", "clean": "rimraf dist", "prepack": "turbo run build", "check-dist": "publint && attw --pack .", @@ -39,7 +39,8 @@ }, "dependencies": { "pg": "catalog:", - "@map-colonies/schemas": "catalog:" + "@map-colonies/schemas": "catalog:", + "@map-colonies/drizzle-utils": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", diff --git a/packages/auth-core/src/db/entities/bundle.ts b/packages/auth-core/src/db/entities/bundle.ts deleted file mode 100644 index aeb56186..00000000 --- a/packages/auth-core/src/db/entities/bundle.ts +++ /dev/null @@ -1,53 +0,0 @@ -// import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; -// import { Environment, type Environments, type IBundle } from '../../model'; - -import { integer, jsonb, text, timestamp } from 'drizzle-orm/pg-core'; -import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; - -// /** -// * The typeorm implementation of the IBundle interface. -// */ -// @Entity() -// export class Bundle implements IBundle { -// @PrimaryColumn({ generated: 'identity', generatedIdentity: 'ALWAYS', insert: false, type: 'integer' }) -// public id!: number; - -// @Column({ type: 'text', nullable: true }) -// public hash?: string; - -// @Column({ type: 'enum', enum: Environment, enumName: 'environment_enum' }) -// public environment!: Environments; - -// @Column({ type: 'jsonb', nullable: true }) -// public metadata?: Record; - -// @Column({ type: 'jsonb', nullable: true }) -// public assets?: { name: string; version: number }[]; - -// @Column({ type: 'jsonb', nullable: true }) -// public connections?: { name: string; version: number }[]; - -// @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) -// public createdAt?: Date; - -// @Column({ name: 'key_version', type: 'integer', nullable: true }) -// public keyVersion?: number; - -// @Column({ name: 'opa_version', type: 'text', nullable: false }) -// public opaVersion!: string; -// } - -export const bundleTable = authManagerSchema.table('bundle', { - id: integer().primaryKey().generatedAlwaysAsIdentity(), - hash: text(), - environment: environmentEnum().notNull(), - metadata: jsonb().$type>(), - assets: jsonb().$type<{ name: string; version: number }[]>(), - connections: jsonb().$type<{ name: string; version: number }[]>(), - createdAt: createdAtColumn, - keyVersion: integer(), - opaVersion: text().notNull(), -}); - -export type Bundle = typeof bundleTable.$inferSelect; -export type NewBundle = typeof bundleTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/client.ts b/packages/auth-core/src/db/entities/client.ts deleted file mode 100644 index 3fa5a929..00000000 --- a/packages/auth-core/src/db/entities/client.ts +++ /dev/null @@ -1,54 +0,0 @@ -// import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; -// import type { IClient, PointOfContact } from '../../model'; - -import { json, text, timestamp } from 'drizzle-orm/pg-core'; -import type { PointOfContact } from '../../model'; -import { authManagerSchema, createdAtColumn } from './common'; - -// /** -// * The typeorm implementation of the IClient interface. -// */ -// @Entity() -// export class Client implements IClient { -// @PrimaryColumn({ name: 'name', type: 'text', unique: true }) -// public name!: string; - -// @Column({ type: 'text', name: 'heb_name' }) -// public hebName!: string; - -// @Column({ type: 'text', nullable: true }) -// public description?: string; - -// @Column({ type: 'text', nullable: true }) -// public branch?: string; - -// @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) -// public createdAt?: Date; - -// @UpdateDateColumn({ name: 'update_at', type: 'timestamptz' }) -// public updatedAt?: Date; - -// @Column({ type: 'json', name: 'tech_point_of_contact', nullable: true }) -// public techPointOfContact?: PointOfContact; - -// @Column({ type: 'json', name: 'product_point_of_contact', nullable: true }) -// public productPointOfContact?: PointOfContact; - -// @Column({ type: 'text', array: true, nullable: true }) -// public tags?: string[] | undefined; -// } - -export const clientTable = authManagerSchema.table('client', { - name: text().primaryKey(), - hebName: text().notNull(), - description: text(), - branch: text(), - createdAt: createdAtColumn, - updatedAt: timestamp('update_at', { withTimezone: true }).defaultNow().notNull(), - techPointOfContact: json('tech_point_of_contact').$type(), - productPointOfContact: json('product_point_of_contact').$type(), - tags: text().array(), -}); - -export type Client = typeof clientTable.$inferSelect; -export type NewClient = typeof clientTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/common.ts b/packages/auth-core/src/db/entities/common.ts deleted file mode 100644 index e3977b43..00000000 --- a/packages/auth-core/src/db/entities/common.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as d from 'drizzle-orm/pg-core'; -import { timestamp } from 'drizzle-orm/pg-core'; - -export const authManagerSchema = d.snakeCase.schema('auth_manager'); -export const environmentEnum = authManagerSchema.enum('environment_enum', ['np', 'stage', 'prod']); - -export const createdAtColumn = timestamp({ withTimezone: true }).defaultNow().notNull(); - -export type Environments = (typeof environmentEnum.enumValues)[number]; diff --git a/packages/auth-core/src/db/entities/connection.ts b/packages/auth-core/src/db/entities/connection.ts deleted file mode 100644 index 4955492c..00000000 --- a/packages/auth-core/src/db/entities/connection.ts +++ /dev/null @@ -1,61 +0,0 @@ -// import { Column, CreateDateColumn, Entity, PrimaryColumn } from 'typeorm'; -// import { Environment, type IConnection, type Environments } from '../../model'; - -import { boolean, integer, primaryKey, text, timestamp, varchar } from 'drizzle-orm/pg-core'; -import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; - -// /** -// * The typeorm implementation of the IConnection interface. -// */ -// @Entity() -// export class Connection implements IConnection { -// @PrimaryColumn({ type: 'varchar' }) -// public name!: string; - -// @PrimaryColumn({ type: 'integer' }) -// public version!: number; - -// @PrimaryColumn({ type: 'enum', enum: Environment, enumName: 'environment_enum' }) -// public environment!: Environments; - -// @Column({ type: 'boolean' }) -// public enabled!: boolean; - -// @Column({ type: 'text' }) -// public token!: string; - -// @Column({ type: 'boolean', name: 'allow_no_browser' }) -// public allowNoBrowserConnection!: boolean; - -// @Column({ type: 'boolean', name: 'allow_no_origin' }) -// public allowNoOriginConnection!: boolean; - -// @Column({ type: 'text', array: true }) -// public domains!: string[]; - -// @Column({ type: 'text', array: true }) -// public origins!: string[]; - -// @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) -// public createdAt!: Date; -// } - -export const connectionTable = authManagerSchema.table( - 'connection', - { - name: varchar().notNull(), - version: integer().notNull(), - environment: environmentEnum().notNull(), - enabled: boolean().notNull(), - token: text().notNull(), - allowNoBrowserConnection: boolean('allow_no_browser').notNull(), - allowNoOriginConnection: boolean('allow_no_origin').notNull(), - domains: text().array().notNull(), - origins: text().array().notNull(), - createdAt: createdAtColumn, - }, - (table) => [primaryKey({ columns: [table.name, table.version, table.environment], name: 'PK_4c3be048a366c9ce9277bac4c38' })] -); - -export type Connection = typeof connectionTable.$inferSelect; -export type NewConnection = typeof connectionTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/domain.ts b/packages/auth-core/src/db/entities/domain.ts deleted file mode 100644 index 4c6ec85f..00000000 --- a/packages/auth-core/src/db/entities/domain.ts +++ /dev/null @@ -1,21 +0,0 @@ -// import { Entity, PrimaryColumn } from 'typeorm'; -// import type { IDomain } from '../../model'; - -import { text } from 'drizzle-orm/pg-core'; -import { authManagerSchema } from './common'; - -// /** -// * The typeorm implementation of the IDomain interface. -// */ -// @Entity() -// export class Domain implements IDomain { -// @PrimaryColumn({ name: 'name', type: 'text', unique: true }) -// public name!: string; -// } - -export const domainTable = authManagerSchema.table('domain', { - name: text().primaryKey(), -}); - -export type Domain = typeof domainTable.$inferSelect; -export type NewDomain = typeof domainTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/key.ts b/packages/auth-core/src/db/entities/key.ts deleted file mode 100644 index 3a194e8d..00000000 --- a/packages/auth-core/src/db/entities/key.ts +++ /dev/null @@ -1,38 +0,0 @@ -// import { Column, Entity, PrimaryColumn } from 'typeorm'; -// import { Environment, type Environments, type IKey, type JWKPrivateKey, type JWKPublicKey } from '../../model'; - -import { integer, jsonb, primaryKey } from 'drizzle-orm/pg-core'; -import type { JWKPrivateKey, JWKPublicKey } from '../../model'; -import { authManagerSchema, environmentEnum } from './common'; - -// /** -// * The typeorm implementation of the IKey interface. -// */ -// @Entity() -// export class Key implements IKey { -// @PrimaryColumn({ type: 'enum', enum: Environment, unique: true, enumName: 'environment_enum' }) -// public environment!: Environments; - -// @PrimaryColumn({ type: 'integer' }) -// public version!: number; - -// @Column({ type: 'jsonb', name: 'private_key' }) -// public privateKey!: JWKPrivateKey; - -// @Column({ type: 'jsonb', name: 'public_key' }) -// public publicKey!: JWKPublicKey; -// } - -export const keyTable = authManagerSchema.table( - 'key', - { - environment: environmentEnum().notNull(), - version: integer().notNull(), - privateKey: jsonb().notNull().$type(), - publicKey: jsonb().notNull().$type(), - }, - (table) => [primaryKey({ columns: [table.environment, table.version], name: 'PK_ddf3d991c46b66651794ee56d58' })] -); - -export type Key = typeof keyTable.$inferSelect; -export type NewKey = typeof keyTable.$inferInsert; diff --git a/packages/auth-core/src/db/index.ts b/packages/auth-core/src/db/index.ts deleted file mode 100644 index cdc18dcd..00000000 --- a/packages/auth-core/src/db/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './entities'; -export * from './utils'; diff --git a/packages/auth-core/src/db/types/index.ts b/packages/auth-core/src/db/types/index.ts deleted file mode 100644 index 95786098..00000000 --- a/packages/auth-core/src/db/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './interfaces'; diff --git a/packages/auth-core/src/db/types/interfaces.ts b/packages/auth-core/src/db/types/interfaces.ts deleted file mode 100644 index 85831779..00000000 --- a/packages/auth-core/src/db/types/interfaces.ts +++ /dev/null @@ -1,8 +0,0 @@ -// import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -// import type { commonDbFullV1Type } from '@map-colonies/schemas'; -// /** -// * An object describing all the necessary configuration to authenticate to a postgresql database. -// * It is an extension of the {@link https://typeorm.io/data-source-options#postgres--cockroachdb-data-source-options | PostgresConnectionOptions} -// * @property ssl include if Should database connection be authenticated using SSL certificates and if true so provide the paths for the SSL certificates and key. -// */ -// export type DbConfig = Pick & PostgresConnectionOptions; diff --git a/packages/auth-core/src/db/utils/createConnection.ts b/packages/auth-core/src/db/utils/createConnection.ts deleted file mode 100644 index 7c90ba19..00000000 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { hostname } from 'node:os'; -import { existsSync, readFileSync, readdirSync } from 'node:fs'; -import path from 'node:path'; -import { Pool, type PoolConfig } from 'pg'; -import type { commonDbFullV1Type } from '@map-colonies/schemas'; -import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { migrate } from 'drizzle-orm/node-postgres/migrator'; -import { relations } from '../entities'; - -export function createConnectionOptions(dbOptions: commonDbFullV1Type): PoolConfig { - const { ssl, ...rest } = dbOptions; - const dbConfig: PoolConfig = structuredClone(rest); - dbConfig.application_name = `${hostname()}-${process.env.NODE_ENV ?? 'unknown_env'}`; - dbConfig.user = dbOptions.username; - if (ssl.enabled) { - dbConfig.password = undefined; - dbConfig.ssl = { key: readFileSync(ssl.key), cert: readFileSync(ssl.cert), ca: readFileSync(ssl.ca) }; - } - - return dbConfig; -} - -export async function initConnection(dbConfig: commonDbFullV1Type): Promise { - const pool = new Pool(createConnectionOptions(dbConfig)); - await pool.query('SELECT NOW()'); - return pool; -} - -export type Drizzle = ReturnType>; - -export type DrizzleTx = Parameters[0]>[0]; - -export function createDrizzle(pool: Pool): Drizzle { - return drizzle({ client: pool, relations }); -} - -export async function runMigrations(drizzle: NodePgDatabase): Promise { - const optionalFolders = ['./db/migrations', './src/db/migrations', './migrations'].map((folder) => path.join(__dirname, '..', '..', '..', folder)); - let migrationsFolder = null; - - for (const folder of optionalFolders) { - if (!existsSync(folder)) { - continue; - } - const folderContent = readdirSync(folder, { withFileTypes: true }); - - const hasMigrations = folderContent.some((item) => item.isDirectory() && existsSync(path.join(folder, item.name, 'migration.sql'))); - if (hasMigrations) { - migrationsFolder = folder; - break; - } - } - - if (migrationsFolder === null) { - throw new Error('No migrations folder found'); - } - - await migrate(drizzle, { migrationsFolder: migrationsFolder, migrationsSchema: 'auth_manager' }); -} diff --git a/packages/auth-core/src/db/utils/index.ts b/packages/auth-core/src/db/utils/index.ts deleted file mode 100644 index 0e2edc9d..00000000 --- a/packages/auth-core/src/db/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './createConnection'; diff --git a/packages/auth-core/src/drizzle.ts b/packages/auth-core/src/drizzle.ts new file mode 100644 index 00000000..471e6bdb --- /dev/null +++ b/packages/auth-core/src/drizzle.ts @@ -0,0 +1,16 @@ +import type { Pool } from 'pg'; +import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { runMigrations as runMigrationsBase } from '@map-colonies/drizzle-utils'; +import { relations } from './entities'; + +export type Drizzle = ReturnType>; + +export type DrizzleTx = Parameters[0]>[0]; + +export function createDrizzle(pool: Pool): Drizzle { + return drizzle({ client: pool, relations }); +} + +export async function runMigrations(drizzle: NodePgDatabase): Promise { + await runMigrationsBase(drizzle, 'auth_manager', __dirname); +} diff --git a/packages/auth-core/src/db/entities/asset.ts b/packages/auth-core/src/entities/asset.ts similarity index 68% rename from packages/auth-core/src/db/entities/asset.ts rename to packages/auth-core/src/entities/asset.ts index ec41a171..ad3a3930 100644 --- a/packages/auth-core/src/db/entities/asset.ts +++ b/packages/auth-core/src/entities/asset.ts @@ -1,6 +1,8 @@ import { boolean, bytea, integer, primaryKey, varchar } from 'drizzle-orm/pg-core'; import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; +type AssetTypeEnumValues = typeof assetTypeEnum.enumValues; + export const assetTypeEnum = authManagerSchema.enum('asset_type_enum', ['TEST', 'TEST_DATA', 'POLICY', 'DATA']); export const assetTable = authManagerSchema.table( @@ -18,5 +20,16 @@ export const assetTable = authManagerSchema.table( (table) => [primaryKey({ columns: [table.name, table.version], name: 'PK_c3670311f777dc6ab9965408f97' })] ); +/* eslint-disable @typescript-eslint/naming-convention */ +export const AssetType: { [K in AssetTypeEnumValues[number]]: K } = { + /** OPA test files. */ + TEST: 'TEST', + TEST_DATA: 'TEST_DATA', + /** OPA policy files. */ + POLICY: 'POLICY', + /** OPA data files, name should end with .json or .yaml. */ + DATA: 'DATA', +} as const; + export type Asset = typeof assetTable.$inferSelect; export type NewAsset = typeof assetTable.$inferInsert; diff --git a/packages/auth-core/src/entities/bundle.ts b/packages/auth-core/src/entities/bundle.ts new file mode 100644 index 00000000..77476ed6 --- /dev/null +++ b/packages/auth-core/src/entities/bundle.ts @@ -0,0 +1,17 @@ +import { integer, jsonb, text } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; + +export const bundleTable = authManagerSchema.table('bundle', { + id: integer().primaryKey().generatedAlwaysAsIdentity(), + hash: text(), + environment: environmentEnum().notNull(), + metadata: jsonb().$type>(), + assets: jsonb().$type<{ name: string; version: number }[]>(), + connections: jsonb().$type<{ name: string; version: number }[]>(), + createdAt: createdAtColumn, + keyVersion: integer(), + opaVersion: text().notNull(), +}); + +export type Bundle = typeof bundleTable.$inferSelect; +export type NewBundle = typeof bundleTable.$inferInsert; diff --git a/packages/auth-core/src/entities/client.ts b/packages/auth-core/src/entities/client.ts new file mode 100644 index 00000000..496619c3 --- /dev/null +++ b/packages/auth-core/src/entities/client.ts @@ -0,0 +1,24 @@ +import { json, text, timestamp } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn } from './common'; + +export interface PointOfContact { + /** The full name. */ + name: string; + phone: string; + email: string; +} + +export const clientTable = authManagerSchema.table('client', { + name: text().primaryKey(), + hebName: text().notNull(), + description: text(), + branch: text(), + createdAt: createdAtColumn, + updatedAt: timestamp('update_at', { withTimezone: true }).defaultNow().notNull(), + techPointOfContact: json('tech_point_of_contact').$type(), + productPointOfContact: json('product_point_of_contact').$type(), + tags: text().array(), +}); + +export type Client = typeof clientTable.$inferSelect; +export type NewClient = typeof clientTable.$inferInsert; diff --git a/packages/auth-core/src/model/common.ts b/packages/auth-core/src/entities/common.ts similarity index 52% rename from packages/auth-core/src/model/common.ts rename to packages/auth-core/src/entities/common.ts index 1e903f50..161769b4 100644 --- a/packages/auth-core/src/model/common.ts +++ b/packages/auth-core/src/entities/common.ts @@ -1,7 +1,13 @@ -import type { environmentEnum } from '../db'; +import * as d from 'drizzle-orm/pg-core'; +import { timestamp } from 'drizzle-orm/pg-core'; type EnvironmentValues = typeof environmentEnum.enumValues; -/** The possible authentication deployment environments. */ + +export const authManagerSchema = d.snakeCase.schema('auth_manager'); +export const environmentEnum = authManagerSchema.enum('environment_enum', ['np', 'stage', 'prod']); + +export const createdAtColumn = timestamp({ withTimezone: true }).defaultNow().notNull(); + /* eslint-disable @typescript-eslint/naming-convention */ export const Environment: { [K in EnvironmentValues[number] as Uppercase]: K } = { /** Non production, may also be called dev. */ @@ -12,5 +18,4 @@ export const Environment: { [K in EnvironmentValues[number] as Uppercase]: K PROD: 'prod', } as const; /* eslint-enable @typescript-eslint/naming-convention */ - -// export type Environments = (typeof Environment)[keyof typeof Environment]; +export type Environments = (typeof environmentEnum.enumValues)[number]; diff --git a/packages/auth-core/src/entities/connection.ts b/packages/auth-core/src/entities/connection.ts new file mode 100644 index 00000000..ad7b40b3 --- /dev/null +++ b/packages/auth-core/src/entities/connection.ts @@ -0,0 +1,22 @@ +import { boolean, integer, primaryKey, text, varchar } from 'drizzle-orm/pg-core'; +import { authManagerSchema, createdAtColumn, environmentEnum } from './common'; + +export const connectionTable = authManagerSchema.table( + 'connection', + { + name: varchar().notNull(), + version: integer().notNull(), + environment: environmentEnum().notNull(), + enabled: boolean().notNull(), + token: text().notNull(), + allowNoBrowserConnection: boolean('allow_no_browser').notNull(), + allowNoOriginConnection: boolean('allow_no_origin').notNull(), + domains: text().array().notNull(), + origins: text().array().notNull(), + createdAt: createdAtColumn, + }, + (table) => [primaryKey({ columns: [table.name, table.version, table.environment], name: 'PK_4c3be048a366c9ce9277bac4c38' })] +); + +export type Connection = typeof connectionTable.$inferSelect; +export type NewConnection = typeof connectionTable.$inferInsert; diff --git a/packages/auth-core/src/entities/domain.ts b/packages/auth-core/src/entities/domain.ts new file mode 100644 index 00000000..593c176b --- /dev/null +++ b/packages/auth-core/src/entities/domain.ts @@ -0,0 +1,9 @@ +import { text } from 'drizzle-orm/pg-core'; +import { authManagerSchema } from './common'; + +export const domainTable = authManagerSchema.table('domain', { + name: text().primaryKey(), +}); + +export type Domain = typeof domainTable.$inferSelect; +export type NewDomain = typeof domainTable.$inferInsert; diff --git a/packages/auth-core/src/db/entities/index.ts b/packages/auth-core/src/entities/index.ts similarity index 100% rename from packages/auth-core/src/db/entities/index.ts rename to packages/auth-core/src/entities/index.ts diff --git a/packages/auth-core/src/entities/key.ts b/packages/auth-core/src/entities/key.ts new file mode 100644 index 00000000..56ab91e7 --- /dev/null +++ b/packages/auth-core/src/entities/key.ts @@ -0,0 +1,39 @@ +import { integer, jsonb, primaryKey } from 'drizzle-orm/pg-core'; +import { authManagerSchema, environmentEnum } from './common'; + +/** + * JSON representation of a public key + */ +export interface JWKPublicKey { + kty: string; + n: string; + e: string; + alg: string; + kid: string; +} + +/** + * JSON representation of a private key + */ +export interface JWKPrivateKey extends JWKPublicKey { + d: string; + p: string; + q: string; + dp: string; + dq: string; + qi: string; +} + +export const keyTable = authManagerSchema.table( + 'key', + { + environment: environmentEnum().notNull(), + version: integer().notNull(), + privateKey: jsonb().notNull().$type(), + publicKey: jsonb().notNull().$type(), + }, + (table) => [primaryKey({ columns: [table.environment, table.version], name: 'PK_ddf3d991c46b66651794ee56d58' })] +); + +export type Key = typeof keyTable.$inferSelect; +export type NewKey = typeof keyTable.$inferInsert; diff --git a/packages/auth-core/src/index.ts b/packages/auth-core/src/index.ts index 3e8b66d8..732b4b1c 100644 --- a/packages/auth-core/src/index.ts +++ b/packages/auth-core/src/index.ts @@ -1,2 +1,2 @@ -export * from './db'; -export * from './model'; +export * from './entities'; +export * from './drizzle'; diff --git a/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql b/packages/auth-core/src/migrations/20260513061159_curious_speedball/migration.sql similarity index 100% rename from packages/auth-core/src/db/migrations/20260513061159_curious_speedball/migration.sql rename to packages/auth-core/src/migrations/20260513061159_curious_speedball/migration.sql diff --git a/packages/auth-core/src/db/migrations/20260513061159_curious_speedball/snapshot.json b/packages/auth-core/src/migrations/20260513061159_curious_speedball/snapshot.json similarity index 100% rename from packages/auth-core/src/db/migrations/20260513061159_curious_speedball/snapshot.json rename to packages/auth-core/src/migrations/20260513061159_curious_speedball/snapshot.json diff --git a/packages/auth-core/src/model/asset.ts b/packages/auth-core/src/model/asset.ts deleted file mode 100644 index 24098a8c..00000000 --- a/packages/auth-core/src/model/asset.ts +++ /dev/null @@ -1,41 +0,0 @@ -// import type { Environments } from './common'; - -import type { assetTypeEnum } from '../db'; - -/* eslint-disable @typescript-eslint/naming-convention */ -type assetTypeEnumValues = typeof assetTypeEnum.enumValues; -export const AssetType: { [K in assetTypeEnumValues[number]]: K } = { - /** OPA test files. */ - TEST: 'TEST', - TEST_DATA: 'TEST_DATA', - /** OPA policy files. */ - POLICY: 'POLICY', - /** OPA data files, name should end with .json or .yaml. */ - DATA: 'DATA', -} as const; -/* eslint-enable @typescript-eslint/naming-convention */ - -// /** -// * Describes the metadata and content of assets - files that will be part of the bundle. -// */ -// export interface IAsset { -// /** The unique name of the asset. */ -// name: string; -// /** -// * The version of Asset with the given name. Starts at 1 and automatically increments. -// * When updated, the POST body should contain the latest version. -// */ -// version: number; -// /** Automatically generated date when the given asset version was created. */ -// createdAt?: Date; -// /** Base64 encoded value of the asset file. */ -// value: string; -// /** The path inside the bundle the asset will be in. use / for the root of the bundle. */ -// uri: string; -// /** The asset type. */ -// type: AssetTypes; -// /** The environments the asset belongs do. It will be deployed only to the specified environments. */ -// environment: Environments[]; -// /** Whether the file contains a template that should be rendered before inserting to the bundle. */ -// isTemplate: boolean; -// } diff --git a/packages/auth-core/src/model/bundle.ts b/packages/auth-core/src/model/bundle.ts deleted file mode 100644 index 891e89bb..00000000 --- a/packages/auth-core/src/model/bundle.ts +++ /dev/null @@ -1,25 +0,0 @@ -// import type { Environments } from './common'; - -/** - * Describes the metadata of contents of bundles that were created. - */ -// export interface IBundle { -// /** The auto-generated ID of the bundle. */ -// id?: number; -// /** The environment the bundle was created for. */ -// environment: Environments; -// /** The md5 based hash of the bundle tarball. */ -// hash?: string; -// /** Free form object to describe the bundle. */ -// metadata?: Record; -// /** A list of all the assets that are part of the bundle. */ -// assets?: { name: string; version: number }[]; -// /** A list of all the connections that are part of the bundle. */ -// connections?: { name: string; version: number }[]; -// /** Automatically generated date when the given bundle was created. */ -// createdAt?: Date; -// /** The version of the key that is part of the bundle. */ -// keyVersion?: number; -// /** The version of the OPA cli that was used to create the bundle. */ -// opaVersion: string; -// } diff --git a/packages/auth-core/src/model/client.ts b/packages/auth-core/src/model/client.ts deleted file mode 100644 index dfc3c4ea..00000000 --- a/packages/auth-core/src/model/client.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Describes The contact information of a specific person. - */ -export interface PointOfContact { - /** The full name. */ - name: string; - phone: string; - email: string; -} - -// /** -// * Describes a specific authentication client. e.g. a system not a person. -// */ -// export interface IClient { -// /** The name of the client. */ -// name: string; -// /** The name of the client in hebrew. */ -// hebName: string; -// /** A short description about the client. */ -// description?: string; -// /** The branch the client belongs to. */ -// branch?: string; -// /** Automatically generated date of when the given client was created at. */ -// createdAt?: Date; -// /** Automatically generated date of when the given client was updated at. */ -// updatedAt?: Date; -// /** The contact details of the person in charge of tech at the client. */ -// techPointOfContact?: PointOfContact; -// /** The contact details of the person in charge of product at the client. */ -// productPointOfContact?: PointOfContact; -// /** The tags describing the client. */ -// tags?: string[]; -// } diff --git a/packages/auth-core/src/model/connection.ts b/packages/auth-core/src/model/connection.ts deleted file mode 100644 index d36f9be0..00000000 --- a/packages/auth-core/src/model/connection.ts +++ /dev/null @@ -1,31 +0,0 @@ -// import type { Environments } from './common'; - -/** - * A connection is the object describing the details of - * how a specific clients authenticates to a specific {@link Environment}. - */ -// export interface IConnection { -// /** The name of the clients this connection relates to. */ -// name: string; -// /** -// * The version of the connection with the given {@link name} and {@link environment}. Starts at 1 and automatically increments. -// * When updated, the POST body should contain the latest version. -// */ -// version: number; -// /** The environment this connection relates to. */ -// environment: Environments; -// /** Automatically generated date of when the given connection version was created at. */ -// createdAt?: Date; -// /** Is the connection enabled. If it is not, it wwill be ignored when creating a new bundle. */ -// enabled: boolean; -// /** The client's token for the specific environment. The KID parameter in the token should equal the client's name. */ -// token: string; -// /** Decides if requests that are not originated from a browser are allowed. */ -// allowNoBrowserConnection: boolean; -// /** Decides if requests that are missing the Origin header are allowed. */ -// allowNoOriginConnection: boolean; -// /** A list of domains that the client is allowed to send request to. */ -// domains: string[]; -// /** A list of origins the client is allowed to send requests from. */ -// origins: string[]; -// } diff --git a/packages/auth-core/src/model/domain.ts b/packages/auth-core/src/model/domain.ts deleted file mode 100644 index bc112164..00000000 --- a/packages/auth-core/src/model/domain.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * A domain describes a part of the MapColonies system. - */ -// export interface IDomain { -// name: string; -// } diff --git a/packages/auth-core/src/model/index.ts b/packages/auth-core/src/model/index.ts deleted file mode 100644 index bf185f22..00000000 --- a/packages/auth-core/src/model/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './asset'; -export * from './bundle'; -export * from './client'; -export * from './connection'; -export * from './domain'; -export * from './key'; -export * from './common'; diff --git a/packages/auth-core/src/model/key.ts b/packages/auth-core/src/model/key.ts deleted file mode 100644 index d8e324cc..00000000 --- a/packages/auth-core/src/model/key.ts +++ /dev/null @@ -1,39 +0,0 @@ -// import type { Environments } from './common'; - -/** - * JSON representation of a public key - */ -export interface JWKPublicKey { - kty: string; - n: string; - e: string; - alg: string; - kid: string; -} - -/** - * JSON representation of a private key - */ -export interface JWKPrivateKey extends JWKPublicKey { - d: string; - p: string; - q: string; - dp: string; - dq: string; - qi: string; -} - -/** - * A representation of a authentication key for a specific environment. - */ -// export interface IKey { -// /** -// * The version of the key with the given {@link environment}. Starts at 1 and automatically increments. -// * When updated, the POST body should contain the latest version. -// */ -// environment: Environments; -// /** The environment this key relates to. */ -// version: number; -// privateKey: JWKPrivateKey; -// publicKey: JWKPublicKey; -// } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a40e514e..80548622 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ catalogs: '@map-colonies/config': specifier: 4.0.1 version: 4.0.1 + '@map-colonies/drizzle-utils': + specifier: /home/oferade/repos/libot/infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz + version: 0.1.0 '@map-colonies/error-express-handler': specifier: ^4.0.0 version: 4.0.0 @@ -245,6 +248,9 @@ importers: '@map-colonies/config': specifier: 'catalog:' version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) @@ -333,6 +339,9 @@ importers: '@map-colonies/config': specifier: 'catalog:' version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/error-express-handler': specifier: 'catalog:' version: 4.0.0 @@ -735,6 +744,9 @@ importers: '@map-colonies/config': specifier: 'catalog:' version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/error-express-handler': specifier: 'catalog:' version: 4.0.0 @@ -895,9 +907,6 @@ importers: test-utils: specifier: workspace:^ version: link:../../packages/test-utils - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) tsc-alias: specifier: 'catalog:' version: 1.8.17 @@ -913,6 +922,9 @@ importers: '@map-colonies/auth-core': specifier: workspace:^ version: link:../auth-core + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) @@ -974,6 +986,9 @@ importers: packages/auth-core: dependencies: + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) @@ -2357,6 +2372,14 @@ packages: prom-client: optional: true + '@map-colonies/drizzle-utils@file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz': + resolution: {integrity: sha512-J2m+qhIkTEOQea3emoPo78B2HLAf8vBSvoq0IfV0WQ7FMTbXSFNWyYH2IMBY4dQnvASeJWTiTb3oXRs/XP53xQ==, tarball: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz} + version: 0.1.0 + engines: {node: '>=24'} + peerDependencies: + drizzle-orm: ^1.0.0-rc.2 + pg: ^8.20.0 + '@map-colonies/error-express-handler@4.0.0': resolution: {integrity: sha512-3vDxDN0IlBy6gRBIyh5/fiKRQeJV2DZDJDEckPoBCADtWSdtzKhM+F+c3HdSru/8tm4lPYeDS9JVeOPJk419Ag==} engines: {node: '>=24'} @@ -11198,6 +11221,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@map-colonies/drizzle-utils@file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0)': + dependencies: + '@map-colonies/schemas': 1.21.0 + '@types/pg': 8.20.0 + drizzle-orm: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) + pg: 8.20.0 + '@map-colonies/error-express-handler@4.0.0': dependencies: http-status-codes: 2.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d02eeca2..d57c17df 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,7 +26,6 @@ catalog: '@map-colonies/vitest-utils': '0.2.0' '@faker-js/faker': '^9.7.0' '@aws-sdk/client-s3': '^3.317.0' - 'typeorm': '^0.3.12' 'pg': '^8.20.0' '@types/pg': '^8.20.0' '@opentelemetry/api': '^1.9.0' @@ -55,3 +54,4 @@ catalog: 'tsc-alias': '^1.8.17' 'prom-client': '15.1.3' 'jose': '6.2.3' + '@map-colonies/drizzle-utils': '/home/oferade/repos/libot/infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz' diff --git a/turbo.json b/turbo.json index b723c00e..9f47af62 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,7 @@ { "$schema": "https://turborepo.com/schema.json", "ui": "stream", + "concurrency": "4", "globalDependencies": ["turbo.json", "pnpm-workspace.yaml", "vitest.config.ts"], "tasks": { "build": { From aee8a57e7971e5e5060b9dcd325cc49b5e3830e6 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Thu, 28 May 2026 11:32:59 +0300 Subject: [PATCH 10/14] chore(global): removed comments --- apps/auth-manager/src/common/constants.ts | 6 ----- .../connection/models/connectionManager.ts | 6 ----- apps/auth-manager/src/containerConfig.ts | 26 ------------------- .../tests/integration/docs/docs.spec.ts | 6 ++--- packages/auth-bundler/src/errors.ts | 7 ----- 5 files changed, 3 insertions(+), 48 deletions(-) diff --git a/apps/auth-manager/src/common/constants.ts b/apps/auth-manager/src/common/constants.ts index 96877ca5..b43917df 100644 --- a/apps/auth-manager/src/common/constants.ts +++ b/apps/auth-manager/src/common/constants.ts @@ -17,11 +17,5 @@ export const SERVICES = { METRICS: Symbol('Metrics'), HEALTHCHECK: Symbol('Healthcheck'), DRIZZLE: Symbol('Drizzle'), - // DOMAIN_REPOSITORY: Symbol('DOMAIN_REPO'), - // CLIENT_REPOSITORY: Symbol('CLIENT_REPO'), - // KEY_REPOSITORY: Symbol('KEY_REPO'), - // ASSET_REPOSITORY: Symbol('ASSET_REPO'), - // CONNECTION_REPOSITORY: Symbol('CONNECTION_REPO'), - // BUNDLE_REPOSITORY: Symbol('BUNDLE_REPO'), } satisfies Record; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/apps/auth-manager/src/connection/models/connectionManager.ts b/apps/auth-manager/src/connection/models/connectionManager.ts index 15328606..57192131 100644 --- a/apps/auth-manager/src/connection/models/connectionManager.ts +++ b/apps/auth-manager/src/connection/models/connectionManager.ts @@ -23,27 +23,21 @@ type ConnectionSearchParams = NonNullable :domains', { domains: params.domains }); filters.push(arrayContains(connectionTable.domains, params.domains)); } if (params.isEnabled !== undefined) { - // qb.andWhere('connection.enabled = :isEnabled', { isEnabled: params.isEnabled }); filters.push(eq(connectionTable.enabled, params.isEnabled)); } return and(...filters); diff --git a/apps/auth-manager/src/containerConfig.ts b/apps/auth-manager/src/containerConfig.ts index a482a7c6..a386ea50 100644 --- a/apps/auth-manager/src/containerConfig.ts +++ b/apps/auth-manager/src/containerConfig.ts @@ -68,37 +68,11 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise }), }, }, - // { - // token: SERVICES.DOMAIN_REPOSITORY, - // provider: { useFactory: instanceCachingFactory(domainRepositoryFactory) }, - // }, { token: DOMAIN_ROUTER_SYMBOL, provider: { useFactory: domainRouterFactory } }, - // { - // token: SERVICES.CLIENT_REPOSITORY, - // provider: { useFactory: instanceCachingFactory(clientRepositoryFactory) }, - // }, { token: CLIENT_ROUTER_SYMBOL, provider: { useFactory: clientRouterFactory } }, - // { - // token: SERVICES.KEY_REPOSITORY, - // provider: { useFactory: instanceCachingFactory(keyRepositoryFactory) }, - // }, { token: KEY_ROUTER_SYMBOL, provider: { useFactory: keyRouterFactory } }, - // { - // token: SERVICES.ASSET_REPOSITORY, - // provider: { useFactory: instanceCachingFactory(assetRepositoryFactory) }, - // }, { token: ASSET_ROUTER_SYMBOL, provider: { useFactory: assetRouterFactory } }, - // { - // token: SERVICES.CONNECTION_REPOSITORY, - // provider: { useFactory: instanceCachingFactory(connectionRepositoryFactory) }, - // }, { token: CONNECTION_ROUTER_SYMBOL, provider: { useFactory: connectionRouterFactory } }, - // { - // token: SERVICES.BUNDLE_REPOSITORY, - // provider: { - // useFactory: instanceCachingFactory((c) => c.resolve(DataSource).getRepository(Bundle)), - // }, - // }, { token: BUNDLE_ROUTER_SYMBOL, provider: { useFactory: bundleRouterFactory } }, { token: 'onSignal', diff --git a/apps/auth-manager/tests/integration/docs/docs.spec.ts b/apps/auth-manager/tests/integration/docs/docs.spec.ts index 94cc1d20..918a72dc 100644 --- a/apps/auth-manager/tests/integration/docs/docs.spec.ts +++ b/apps/auth-manager/tests/integration/docs/docs.spec.ts @@ -4,9 +4,9 @@ import { trace } from '@opentelemetry/api'; import httpStatusCodes from 'http-status-codes'; import type { DependencyContainer } from 'tsyringe'; import { Pool } from 'pg'; -import { getApp } from '../../../src/app'; -import { initConfig } from '../../../src/common/config'; -import { SERVICES } from '../../../src/common/constants'; +import { getApp } from '@src/app'; +import { initConfig } from '@src/common/config'; +import { SERVICES } from '@src/common/constants'; import { DocsRequestSender } from './helpers/docsRequestSender'; describe('docs', function () { diff --git a/packages/auth-bundler/src/errors.ts b/packages/auth-bundler/src/errors.ts index 683eb3fe..a4807828 100644 --- a/packages/auth-bundler/src/errors.ts +++ b/packages/auth-bundler/src/errors.ts @@ -5,13 +5,6 @@ export class MissingPolicyFilesError extends Error { } } -// export class ConnectionNotInitializedError extends Error { -// public constructor(message: string) { -// super(message); -// Object.setPrototypeOf(this, ConnectionNotInitializedError.prototype); -// } -// } - export class KeyNotFoundError extends Error { public constructor(message: string) { super(message); From a082ebf4f4a4c951c3ee2817622e73ff97c1a058 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Sun, 31 May 2026 09:50:28 +0300 Subject: [PATCH 11/14] chore(auth-core): fixed order of imports --- packages/auth-core/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index 5f0b8789..6e5e4cc5 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -14,8 +14,8 @@ "exports": { ".": { "import": { - "default": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "default": "./dist/index.js" }, "require": "./dist/index.js" } From a56f8f1a1a3263cd03b27a6eb003029a8e7c3007 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Sun, 31 May 2026 10:41:46 +0300 Subject: [PATCH 12/14] deps(global): updated drizzle utils --- pnpm-lock.yaml | 21 ++++++++++----------- pnpm-workspace.yaml | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80548622..aebaef7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ catalogs: specifier: 4.0.1 version: 4.0.1 '@map-colonies/drizzle-utils': - specifier: /home/oferade/repos/libot/infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz - version: 0.1.0 + specifier: 0.2.0 + version: 0.2.0 '@map-colonies/error-express-handler': specifier: ^4.0.0 version: 4.0.0 @@ -250,7 +250,7 @@ importers: version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) '@map-colonies/drizzle-utils': specifier: 'catalog:' - version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) + version: 0.2.0(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) @@ -341,7 +341,7 @@ importers: version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) '@map-colonies/drizzle-utils': specifier: 'catalog:' - version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) + version: 0.2.0(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/error-express-handler': specifier: 'catalog:' version: 4.0.0 @@ -746,7 +746,7 @@ importers: version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) '@map-colonies/drizzle-utils': specifier: 'catalog:' - version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) + version: 0.2.0(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/error-express-handler': specifier: 'catalog:' version: 4.0.0 @@ -924,7 +924,7 @@ importers: version: link:../auth-core '@map-colonies/drizzle-utils': specifier: 'catalog:' - version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) + version: 0.2.0(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) @@ -988,7 +988,7 @@ importers: dependencies: '@map-colonies/drizzle-utils': specifier: 'catalog:' - version: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) + version: 0.2.0(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0) '@map-colonies/js-logger': specifier: 'catalog:' version: 5.0.0(@opentelemetry/api@1.9.0) @@ -2372,9 +2372,8 @@ packages: prom-client: optional: true - '@map-colonies/drizzle-utils@file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz': - resolution: {integrity: sha512-J2m+qhIkTEOQea3emoPo78B2HLAf8vBSvoq0IfV0WQ7FMTbXSFNWyYH2IMBY4dQnvASeJWTiTb3oXRs/XP53xQ==, tarball: file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz} - version: 0.1.0 + '@map-colonies/drizzle-utils@0.2.0': + resolution: {integrity: sha512-mNZLc8TOI3iSuuAB7D2m0/fVVBGZfuYKSSjzzNV75pCxdNoGDiuoIJnnrVPzk+DhLpIqfYwqpJlGFwCVPBy/3Q==} engines: {node: '>=24'} peerDependencies: drizzle-orm: ^1.0.0-rc.2 @@ -11221,7 +11220,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@map-colonies/drizzle-utils@file:../infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0)': + '@map-colonies/drizzle-utils@0.2.0(drizzle-orm@1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3))(pg@8.20.0)': dependencies: '@map-colonies/schemas': 1.21.0 '@types/pg': 8.20.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d57c17df..b3366012 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,6 +18,7 @@ catalog: '@map-colonies/error-express-handler': '^4.0.0' '@map-colonies/express-access-log-middleware': '^4.1.0' '@map-colonies/openapi-express-viewer': '^5.0.0' + '@map-colonies/drizzle-utils': '0.2.0' 'vitest': '4.1.4' '@vitest/coverage-v8': '4.1.4' '@vitest/ui': '4.1.4' @@ -54,4 +55,3 @@ catalog: 'tsc-alias': '^1.8.17' 'prom-client': '15.1.3' 'jose': '6.2.3' - '@map-colonies/drizzle-utils': '/home/oferade/repos/libot/infra-packages/packages/drizzle-utils/map-colonies-drizzle-utils-0.1.2.tgz' From 2ae8f928fb25aa9c972ad25d0f9713abcf102ab4 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:02:57 +0300 Subject: [PATCH 13/14] chore(global): pr comments --- apps/auth-cron/dataSource.mjs | 33 ----------- apps/auth-cron/package.json | 1 - .../src/asset/DAL/assetRepository.ts | 14 ++--- apps/auth-manager/src/common/constants.ts | 2 - .../connection/models/connectionManager.ts | 3 +- apps/auth-ui/package.json | 2 +- knip.config.ts | 10 ++-- packages/auth-core/drizzle.config.mts | 4 +- packages/auth-core/package.json | 4 +- packages/auth-core/runMigrations.mts | 21 +++++++ packages/test-utils/package.json | 4 +- packages/test-utils/src/drizzle.ts | 1 - pnpm-lock.yaml | 59 +++++++++---------- pnpm-workspace.yaml | 1 + turbo.json | 2 +- 15 files changed, 69 insertions(+), 92 deletions(-) delete mode 100644 apps/auth-cron/dataSource.mjs create mode 100644 packages/auth-core/runMigrations.mts diff --git a/apps/auth-cron/dataSource.mjs b/apps/auth-cron/dataSource.mjs deleted file mode 100644 index 77a4276b..00000000 --- a/apps/auth-cron/dataSource.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { DataSource } from 'typeorm'; -import { createConnectionOptions } from '@map-colonies/auth-core'; - -/** - * - * @param {string} moduleName - * @returns {Promise} - */ -async function importModule(moduleName) { - let imported; - try { - imported = await import(`./${moduleName}`); - } catch (err) { - if (err instanceof Error && 'code' in err && err.code === 'ERR_MODULE_NOT_FOUND') { - imported = await import(`./dist/${moduleName}`); - } else { - throw err; - } - } - return imported; -} - -const { getConfig, initConfig } = await importModule('config.js'); - -await initConfig(); -const configOption = getConfig().get('db'); -const connectionOptions = configOption; - -const appDataSource = new DataSource({ - ...createConnectionOptions(connectionOptions), -}); - -export default appDataSource; diff --git a/apps/auth-cron/package.json b/apps/auth-cron/package.json index efe7ead5..f2e5abb9 100644 --- a/apps/auth-cron/package.json +++ b/apps/auth-cron/package.json @@ -54,7 +54,6 @@ "@types/express": "catalog:", "jest-extended": "catalog:", "test-utils": "workspace:^", - "ts-node": "^10.9.1", "@types/pg": "catalog:", "typescript": "catalog:", "@types/lodash": "catalog:", diff --git a/apps/auth-manager/src/asset/DAL/assetRepository.ts b/apps/auth-manager/src/asset/DAL/assetRepository.ts index 7f80ddd0..a30ea563 100644 --- a/apps/auth-manager/src/asset/DAL/assetRepository.ts +++ b/apps/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,4 +1,4 @@ -import { and, eq, max } from 'drizzle-orm'; +import { and, desc, eq, max } from 'drizzle-orm'; import { assetTable, type DrizzleTx, type Drizzle } from '@map-colonies/auth-core'; import { inject, Lifecycle, scoped } from 'tsyringe'; import { SERVICES } from '@common/constants'; @@ -8,17 +8,13 @@ export class AssetRepository { public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} public async getMaxVersionWithLock(name: string, tx: DrizzleTx): Promise { - const subQuery = tx - .select({ maxVersion: max(assetTable.version) }) - .from(assetTable) - .where(eq(assetTable.name, name)); - const result = await tx .select({ version: assetTable.version }) .from(assetTable) - .where(and(eq(assetTable.name, name), eq(assetTable.version, subQuery))) - .for('update') - .limit(1); + .where(eq(assetTable.name, name)) + .orderBy(desc(assetTable.version)) + .limit(1) + .for('update'); return result[0]?.version ?? null; } diff --git a/apps/auth-manager/src/common/constants.ts b/apps/auth-manager/src/common/constants.ts index b43917df..0681e464 100644 --- a/apps/auth-manager/src/common/constants.ts +++ b/apps/auth-manager/src/common/constants.ts @@ -2,8 +2,6 @@ import { readPackageJsonSync } from '@map-colonies/read-pkg'; export const SERVICE_NAME = readPackageJsonSync().name ?? 'unknown_service'; -export const DB_CONNECTION_TIMEOUT = 5000; - export const TOKENS_ISSUER = 'mapcolonies-token-cli'; export const IGNORED_OUTGOING_TRACE_ROUTES = [/^.*\/v1\/metrics.*$/]; diff --git a/apps/auth-manager/src/connection/models/connectionManager.ts b/apps/auth-manager/src/connection/models/connectionManager.ts index 57192131..1bd7a585 100644 --- a/apps/auth-manager/src/connection/models/connectionManager.ts +++ b/apps/auth-manager/src/connection/models/connectionManager.ts @@ -19,8 +19,7 @@ import { ConnectionVersionMismatchError, ConnectionNotFoundError } from './error type ConnectionSearchParams = NonNullable; -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function getSearchFilters(params: ConnectionSearchParams) { +function getSearchFilters(params: ConnectionSearchParams): SQL | undefined { const filters: SQL[] = []; if (params.name !== undefined) { filters.push(ilike(connectionTable.name, `%${params.name}%`)); diff --git a/apps/auth-ui/package.json b/apps/auth-ui/package.json index e50c1cbe..0ff78c5e 100644 --- a/apps/auth-ui/package.json +++ b/apps/auth-ui/package.json @@ -50,7 +50,7 @@ "tailwindcss": "^4.1.3", "tw-animate-css": "^1.2.5", "zod": "^3.24.4", - "lodash": "^4.17.23", + "lodash": "catalog:", "tailwindcss-animate": "^1.0.7" }, "devDependencies": { diff --git a/knip.config.ts b/knip.config.ts index 553e1e8c..bf85fb2d 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -14,10 +14,10 @@ const config: KnipConfig = { ignoreDependencies: ['@map-colonies/infra-copilot-instructions', '@vitest/eslint-plugin'], workspaces: { 'packages/auth-core': { - entry: ['src/config.ts', 'src/db/migrations/*', 'dataSource.{ts,mjs}'], + entry: ['src/config.ts'], + ignoreFiles: ['drizzle.config.mts'], }, 'packages/auth-bundler': { - entry: ['dataSource.ts'], ignore: ['example/**'], ignoreBinaries: ['opa'], }, @@ -25,14 +25,16 @@ const config: KnipConfig = { 'apps/auth-manager': { ignoreUnresolved: ['./instrumentation.mjs'], ignoreDependencies: ['@types/lodash'], - entry: ['src/instrumentation.mts', 'dataSource.mjs'], + ignoreFiles: ['src/runMigrations.mts'], + entry: ['src/instrumentation.mts'], }, 'apps/auth-cron': { ignoreUnresolved: ['./instrumentation.mjs'], - entry: ['src/instrumentation.mts', 'dataSource.mjs'], + entry: ['src/instrumentation.mts'], }, 'apps/token-kiosk': { ignoreUnresolved: ['./instrumentation.mjs'], + ignoreFiles: ['src/runMigrations.mts'], entry: ['src/instrumentation.mts', 'drizzle.config.mts'], }, 'apps/kiosk-ui': { diff --git a/packages/auth-core/drizzle.config.mts b/packages/auth-core/drizzle.config.mts index 8cacb79c..50a7192d 100644 --- a/packages/auth-core/drizzle.config.mts +++ b/packages/auth-core/drizzle.config.mts @@ -15,8 +15,8 @@ const dbOptions = createConnectionOptions(config) as Omit { + const configInstance = await config({ + schema: commonDbFullV2, + offlineMode: true, + }); + + const pool = await initConnection(configInstance.getAll()); + await runMigrations(createDrizzle(pool)); + await pool.end(); + console.log('Migrations completed'); +})().catch((err) => { + console.error('Failed to run migrations', err); + process.exit(1); +}); diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5bbdd9be..66ad46bf 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -37,7 +37,7 @@ "@map-colonies/schemas": "catalog:", "@testcontainers/minio": "~11.14.0", "@testcontainers/postgresql": "^11.14.0", - "lodash": "^4.18.1", + "lodash": "catalog:", "drizzle-orm": "catalog:", "pg": "catalog:" }, @@ -51,6 +51,6 @@ "@types/pg": "catalog:", "eslint": "catalog:", "typescript": "catalog:", - "@types/lodash": "^4.17.24" + "@types/lodash": "catalog:" } } diff --git a/packages/test-utils/src/drizzle.ts b/packages/test-utils/src/drizzle.ts index 05e9b166..8f331eef 100644 --- a/packages/test-utils/src/drizzle.ts +++ b/packages/test-utils/src/drizzle.ts @@ -7,7 +7,6 @@ import type { commonDbFullV1Type } from '@map-colonies/schemas'; */ export async function resetAndMigrate(connection: Pool): Promise { await connection.query(`DROP SCHEMA IF EXISTS "${authManagerSchema.schemaName}" CASCADE`); - // await connection.query(`CREATE SCHEMA "${authManagerSchema.schemaName}"`); const db = createDrizzle(connection); await runMigrations(db); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aebaef7a..fc59a7b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,6 +129,9 @@ catalogs: jose: specifier: 6.2.3 version: 6.2.3 + lodash: + specifier: 4.17.23 + version: 4.17.23 nock: specifier: ^14.0.14 version: 14.0.14 @@ -287,13 +290,13 @@ importers: devDependencies: '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/tsconfig': specifier: 'catalog:' version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/express': specifier: 'catalog:' version: 4.17.25 @@ -314,13 +317,10 @@ importers: version: 4.1.4(vitest@4.1.4) jest-extended: specifier: 'catalog:' - version: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) + version: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) test-utils: specifier: workspace:^ version: link:../../packages/test-utils - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) typescript: specifier: 'catalog:' version: 5.9.3 @@ -420,7 +420,7 @@ importers: version: 9.9.0 '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/openapi-helpers': specifier: 'catalog:' version: 5.1.0(@types/express@4.17.25)(@types/json-schema@7.0.15)(encoding@0.1.13)(openapi-typescript@7.13.0(typescript@5.9.3))(prettier@3.8.1)(supertest@7.2.2)(typescript@5.9.3) @@ -429,7 +429,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/body-parser': specifier: 'catalog:' version: 1.19.6 @@ -462,7 +462,7 @@ importers: version: 4.1.4(vitest@4.1.4) jest-extended: specifier: 'catalog:' - version: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) + version: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) jest-openapi: specifier: 'catalog:' version: 0.14.2 @@ -536,7 +536,7 @@ importers: specifier: ^3.6.0 version: 3.6.0 lodash: - specifier: ^4.17.23 + specifier: 'catalog:' version: 4.17.23 lucide-react: specifier: ^0.488.0 @@ -846,7 +846,7 @@ importers: devDependencies: '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/openapi-helpers': specifier: 'catalog:' version: 5.1.0(@types/express@4.17.25)(@types/json-schema@7.0.15)(encoding@0.1.13)(openapi-typescript@7.13.0(typescript@5.9.3))(prettier@3.8.1)(supertest@7.2.2)(typescript@5.9.3) @@ -855,7 +855,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/body-parser': specifier: 'catalog:' version: 1.19.6 @@ -946,7 +946,7 @@ importers: version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/schemas': specifier: 'catalog:' version: 1.21.0 @@ -955,7 +955,7 @@ importers: version: 2.0.0 '@map-colonies/vitest-utils': specifier: 'catalog:' - version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) + version: 0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4) '@types/node': specifier: 'catalog:' version: 24.12.0 @@ -970,7 +970,7 @@ importers: version: 4.1.4(vitest@4.1.4) jest-extended: specifier: 'catalog:' - version: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) + version: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) test-utils: specifier: workspace:^ version: link:../test-utils @@ -1007,7 +1007,7 @@ importers: version: 4.0.1(@map-colonies/schemas@1.21.0)(prom-client@15.1.3) '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/tsconfig': specifier: 'catalog:' version: 2.0.0 @@ -1020,9 +1020,6 @@ importers: drizzle-kit: specifier: 'catalog:' version: 1.0.0-rc.2 - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) typescript: specifier: 'catalog:' version: 5.9.3 @@ -1031,7 +1028,7 @@ importers: devDependencies: '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/openapi-helpers': specifier: 'catalog:' version: 5.1.0(@types/express@4.17.25)(@types/json-schema@7.0.15)(encoding@0.1.13)(openapi-typescript@7.13.0(typescript@5.9.3))(prettier@3.8.1)(supertest@7.2.2)(typescript@5.9.3) @@ -1063,8 +1060,8 @@ importers: specifier: 'catalog:' version: 1.0.0-rc.2(@opentelemetry/api@1.9.0)(@sinclair/typebox@0.34.48)(@types/pg@8.20.0)(pg@8.20.0)(zod@4.4.3) lodash: - specifier: ^4.18.1 - version: 4.18.1 + specifier: 'catalog:' + version: 4.17.23 pg: specifier: 'catalog:' version: 8.20.0 @@ -1074,12 +1071,12 @@ importers: devDependencies: '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/tsconfig': specifier: 'catalog:' version: 2.0.0 '@types/lodash': - specifier: ^4.17.24 + specifier: 'catalog:' version: 4.17.24 '@types/node': specifier: 'catalog:' @@ -1098,7 +1095,7 @@ importers: devDependencies: '@map-colonies/eslint-config': specifier: 'catalog:' - version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) + version: 8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3) '@map-colonies/openapi-helpers': specifier: 'catalog:' version: 5.1.0(@types/express@4.17.25)(@types/json-schema@7.0.15)(encoding@0.1.13)(openapi-typescript@7.13.0(typescript@5.9.3))(prettier@3.8.1)(supertest@7.2.2)(typescript@5.9.3) @@ -11231,7 +11228,7 @@ snapshots: dependencies: http-status-codes: 2.3.0 - '@map-colonies/eslint-config@8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3)': + '@map-colonies/eslint-config@8.1.0(@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4))(eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(eslint-plugin-react-hooks@5.2.0(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(globals@15.15.0)(typescript@5.9.3)': dependencies: '@eslint/js': 9.39.4 '@map-colonies/eslint-plugin': 0.1.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -11244,7 +11241,7 @@ snapshots: typescript-eslint: 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) optionalDependencies: '@vitest/eslint-plugin': 1.6.15(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4) - eslint-plugin-jest: 28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) + eslint-plugin-jest: 28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.4(jiti@2.6.1)) globals: 15.15.0 transitivePeerDependencies: @@ -11358,11 +11355,11 @@ snapshots: '@map-colonies/tsconfig@2.0.0': {} - '@map-colonies/vitest-utils@0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4)': + '@map-colonies/vitest-utils@0.2.0(jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3))(jest-openapi@0.14.2)(vitest@4.1.4)': dependencies: vitest: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: - jest-extended: 7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3) + jest-extended: 7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3) jest-openapi: 0.14.2 '@monaco-editor/loader@1.7.0': @@ -15306,7 +15303,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3): + eslint-plugin-jest@28.14.0(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.4(jiti@2.6.1) @@ -16454,7 +16451,7 @@ snapshots: jest-util: 29.7.0 optional: true - jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0))(typescript@5.9.3): + jest-extended@7.0.0(jest@29.7.0(@types/node@24.12.0)(ts-node@10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3)))(typescript@5.9.3): dependencies: jest-diff: 30.3.0 typescript: 5.9.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b3366012..3626d866 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -45,6 +45,7 @@ catalog: '@types/compression': '^1.8.1' '@types/express': '^4.17.25' '@types/lodash': '^4.17.24' + lodash: 4.17.23 '@types/body-parser': '1.19.6' '@types/supertest': '^7.2.0' 'cross-env': '^7.0.3' diff --git a/turbo.json b/turbo.json index 9f47af62..a4f8b009 100644 --- a/turbo.json +++ b/turbo.json @@ -2,7 +2,7 @@ "$schema": "https://turborepo.com/schema.json", "ui": "stream", "concurrency": "4", - "globalDependencies": ["turbo.json", "pnpm-workspace.yaml", "vitest.config.ts"], + "globalDependencies": ["turbo.json", "pnpm-workspace.yaml"], "tasks": { "build": { "dependsOn": ["^build"], From 7c1dea081f19c6b4ec1490a2c3a18cc831d1f1e5 Mon Sep 17 00:00:00 2001 From: Schnitz <12687466+CptSchnitz@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:41:47 +0300 Subject: [PATCH 14/14] chore(global): add migrations to eslint ignore --- packages/auth-core/eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/auth-core/eslint.config.mjs b/packages/auth-core/eslint.config.mjs index c60ff3a0..d565e98e 100644 --- a/packages/auth-core/eslint.config.mjs +++ b/packages/auth-core/eslint.config.mjs @@ -1,4 +1,4 @@ import tsBaseConfig from '@map-colonies/eslint-config/ts-base'; import { defineConfig, globalIgnores } from 'eslint/config'; -export default defineConfig(tsBaseConfig, globalIgnores(['drizzle.config.mts', 'vitest.config.mts', '**/migrations/**'])); +export default defineConfig(tsBaseConfig, globalIgnores(['drizzle.config.mts', 'vitest.config.mts', '**/migrations/**', 'runMigrations.mts']));