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 5d7e62ec..f2e5abb9 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", @@ -43,20 +40,21 @@ "@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:", "http-status-codes": "^2.2.0", "pg": "catalog:", "prom-client": "catalog:", - "typeorm": "catalog:" + "drizzle-orm": "catalog:" }, "devDependencies": { "@types/node": "catalog:", "@types/express": "catalog:", "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..ccdbb6e5 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 { 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'; @@ -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 [pool, 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 }); @@ -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/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..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'; @@ -26,10 +24,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 +65,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 +73,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 +81,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 +99,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 +107,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 +115,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 +125,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 +136,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 +154,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 +164,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 +182,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/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/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 1b100954..4615cbc5 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", @@ -51,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:", @@ -65,7 +62,7 @@ "prom-client": "catalog:", "reflect-metadata": "catalog:", "tsyringe": "catalog:", - "typeorm": "catalog:" + "drizzle-orm": "catalog:" }, "devDependencies": { "@map-colonies/openapi-helpers": "catalog:", @@ -84,8 +81,6 @@ "jest-extended": "catalog:", "jest-openapi": "catalog:", "supertest": "catalog:", - "ts-node": "^10.9.1", - "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 cfa9da77..a30ea563 100644 --- a/apps/auth-manager/src/asset/DAL/assetRepository.ts +++ b/apps/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,46 +1,32 @@ -import { Asset } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; +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'; -export type AssetRepository = Repository & { - getMaxVersionWithLock: (name: string) => Promise; - getMaxVersion: (name: string) => Promise; -}; +@scoped(Lifecycle.ContainerScoped) +export class AssetRepository { + public constructor(@inject(SERVICES.DRIZZLE) private readonly db: Drizzle) {} -export const assetRepositoryFactory: FactoryFunction = (container) => { - const dataSource = container.resolve(DataSource); + public async getMaxVersionWithLock(name: string, tx: DrizzleTx): Promise { + const result = await tx + .select({ version: assetTable.version }) + .from(assetTable) + .where(eq(assetTable.name, name)) + .orderBy(desc(assetTable.version)) + .limit(1) + .for('update'); - return dataSource.getRepository(Asset).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(); + return result[0]?.version ?? null; + } - return 'Asset.version = ' + subQuery; - }) - .setLock('pessimistic_write') - .setParameter('name', name) - .getRawOne<{ version: number }>(); + public async getMaxVersion(name: string, tx?: DrizzleTx): Promise { + const db = tx ?? this.db; - 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 }>(); + const result = await db + .select({ version: max(assetTable.version) }) + .from(assetTable) + .where(eq(assetTable.name, name)); - if (result === undefined) { - return null; - } - return result.version; - }, - }); -}; + 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 aeb1792d..ae123da5 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(), }; } @@ -94,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 6e7157df..3ebb6697 100644 --- a/apps/auth-manager/src/asset/models/assetManager.ts +++ b/apps/auth-manager/src/asset/models/assetManager.ts @@ -1,49 +1,52 @@ import { type Logger } from '@map-colonies/js-logger'; -import { IAsset } 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'; -export type ResponseAsset = SetRequired; -export type RequestAsset = Omit; - @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 { + 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.drizzle.query.asset.findMany({ + where: { + isTemplate, + type, + environment: environment ? { arrayContains: environment } : undefined, + }, + }); } - 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 }); + return this.drizzle.query.asset.findMany({ where: { 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 } }); + 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'); } 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,12 +56,11 @@ 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); - 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) { @@ -68,18 +70,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..6c8bb6fc 100644 --- a/apps/auth-manager/src/bundle/models/bundleManager.ts +++ b/apps/auth-manager/src/bundle/models/bundleManager.ts @@ -1,9 +1,7 @@ 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 { SERVICES } from '@common/constants'; -import { createDatesComparison } from '@common/db/utils'; import { BundleSearchParams } from './bundle'; import { BundleNotFoundError } from './errors'; @@ -11,27 +9,29 @@ 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: { + 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.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..7638a825 100644 --- a/apps/auth-manager/src/client/controllers/clientController.ts +++ b/apps/auth-manager/src/client/controllers/clientController.ts @@ -2,21 +2,21 @@ 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 { DEFAULT_PAGE_SIZE, sortOptionParser } from '@map-colonies/drizzle-utils'; import { SERVICES } from '@common/constants'; -import { DEFAULT_PAGE_SIZE } from '@src/common/db/pagination'; -import { sortOptionParser } from '@src/common/db/sort'; +import { removeNulls } from '@src/utils/mapper'; 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 +31,7 @@ function queryParamsToSearchParams(query: NonNullable( +const clientSortMap = new Map( Object.entries({ name: 'name', branch: 'branch', @@ -104,7 +104,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..61610ac9 100644 --- a/apps/auth-manager/src/client/models/clientManager.ts +++ b/apps/auth-manager/src/client/models/clientManager.ts @@ -1,71 +1,71 @@ import { type Logger } from '@map-colonies/js-logger'; import { inject, injectable } from 'tsyringe'; -import { ArrayContains, ILike, QueryFailedError } from 'typeorm'; import { DatabaseError } from 'pg'; -import { Client, type IClient } from '@map-colonies/auth-core'; +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 } 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 { ClientAlreadyExistsError, ClientNotFoundError } from './errors'; import { ClientSearchParams } from './client'; -function isQueryFailedError(err: unknown): err is QueryFailedError { - return typeof err === 'object' && err !== null && 'name' in err && (err as Error).name === 'QueryFailedError'; -} - @injectable() 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] = {}; 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; - } + const whereClause = and( + 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), + createDatesComparison(clientTable.updatedAt, updatedAfter, updatedBefore) + ); + + const { limit, offset } = paginationParamsToOffsetAndLimit(paginationParams); + + const clientsQuery = this.drizzle + .select() + .from(clientTable) + .where(whereClause) + .limit(limit) + .offset(offset) + .orderBy(...sortOptionsToOrderBy(clientTable, sortParams ?? {})); + + const countQuery = this.drizzle.select({ count: count() }).from(clientTable).where(whereClause); + + const [clients, countResult] = await Promise.all([clientsQuery, countQuery]); - return this.clientRepository.findAndCount({ ...findOptions }); + return [clients, countResult[0]?.count ?? 0]; } - 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 +73,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 (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'); @@ -90,19 +90,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/constants.ts b/apps/auth-manager/src/common/constants.ts index ec2c126f..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.*$/]; @@ -16,11 +14,6 @@ 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'), } satisfies Record; /* eslint-enable @typescript-eslint/naming-convention */ 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 44e09ca2..00000000 --- a/apps/auth-manager/src/common/db/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum PgErrorCodes { - 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 94ec83c5..00000000 --- a/apps/auth-manager/src/common/db/pagination.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const DEFAULT_PAGE_SIZE = 10; -export interface PaginationParams { - page: number; - pageSize: number; -} - -export function paginationParamsToFindOptions(paginationParams?: PaginationParams): { take?: number; skip?: number } { - if (paginationParams === undefined) { - return {}; - } - - const { page, pageSize } = paginationParams; - - return { - take: pageSize, - skip: (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 914a569a..00000000 --- a/apps/auth-manager/src/common/db/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FindOperator } from 'typeorm'; -import { Between, LessThan, MoreThan } from 'typeorm'; - -export function createDatesComparison(earlyDate?: Date, laterDate?: Date): FindOperator | undefined { - if (earlyDate !== undefined && laterDate !== undefined) { - return Between(earlyDate, laterDate); - } - if (earlyDate !== undefined) { - return MoreThan(earlyDate); - } - if (laterDate !== undefined) { - return LessThan(laterDate); - } - return undefined; -} 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/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..1cb06840 100644 --- a/apps/auth-manager/src/connection/controllers/connectionController.ts +++ b/apps/auth-manager/src/connection/controllers/connectionController.ts @@ -2,25 +2,24 @@ 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 { 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'; -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..1bd7a585 100644 --- a/apps/auth-manager/src/connection/models/connectionManager.ts +++ b/apps/auth-manager/src/connection/models/connectionManager.ts @@ -1,31 +1,55 @@ 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, 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 { 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 { 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; +function getSearchFilters(params: ConnectionSearchParams): SQL | undefined { + const filters: SQL[] = []; + if (params.name !== undefined) { + filters.push(ilike(connectionTable.name, `%${params.name}%`)); + } + if (params.environment) { + filters.push(inArray(connectionTable.environment, params.environment)); + } + if (params.isNoBrowser !== undefined) { + filters.push(eq(connectionTable.allowNoBrowserConnection, params.isNoBrowser)); + } + if (params.isNoOrigin !== undefined) { + filters.push(eq(connectionTable.allowNoOriginConnection, params.isNoOrigin)); + } + if (params.domains) { + filters.push(arrayContains(connectionTable.domains, params.domains)); + } + if (params.isEnabled !== undefined) { + 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 +66,52 @@ 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'); - - // 1. Apply Base Filters - this.applySearchFilters(qb, searchParams); + const countQuery = + searchParams.onlyLatest === true + ? this.drizzle.select({ count: countDistinct(sql`(${connectionTable.name},${connectionTable.environment})`) }) + : this.drizzle.select({ count: count() }); - // 2. Calculate Total Count - let total: number; + const countResult = await countQuery.from(connectionTable).where(filters); + const total = countResult[0]?.count ?? 0; - 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 selectQuery = + searchParams.onlyLatest === true ? this.drizzle.selectDistinctOn([connectionTable.name, connectionTable.environment]) : this.drizzle.select(); - total = parseInt(countResult?.count ?? '0', 10); - } else { - // STRATEGY: Standard Count - total = await qb.getCount(); - } + const subQuery = selectQuery + .from(connectionTable) + .where(filters) + .orderBy(connectionTable.name, connectionTable.environment, desc(connectionTable.version)) + .as('sq'); - // 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 { limit, offset } = paginationParamsToOffsetAndLimit(paginationParams); - // 4. Pagination & Execution - if (paginationParams) { - const { skip, take } = paginationParamsToFindOptions(paginationParams); - qb.skip(skip).take(take); - } + const connections = await this.drizzle + .select() + .from(subQuery) + .orderBy(...sortOptionsToOrderBy(subQuery, sortParams ?? {})) + .limit(limit) + .offset(offset); - 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.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 +122,25 @@ 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); - 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 === 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) { @@ -154,8 +150,9 @@ 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; } if (maxVersion !== connection.version) { @@ -167,16 +164,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 + .insert(connectionTable) + .values({ ...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 +201,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/containerConfig.ts b/apps/auth-manager/src/containerConfig.ts index 3826307e..a386ea50 100644 --- a/apps/auth-manager/src/containerConfig.ts +++ b/apps/auth-manager/src/containerConfig.ts @@ -3,37 +3,22 @@ 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 { Bundle, initConnection } from '@map-colonies/auth-core'; +import { Pool } from 'pg'; +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 { 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'; -const healthCheck = (connection: DataSource): 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; @@ -47,7 +32,13 @@ 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 { + 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 +49,36 @@ 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, + token: SERVICES.DRIZZLE, provider: { useFactory: instanceCachingFactory((container) => { - const connection = container.resolve(DataSource); - return healthCheck(connection); + const pool = container.resolve(Pool); + return createDrizzle(pool); }), }, }, { - token: SERVICES.DOMAIN_REPOSITORY, - provider: { useFactory: instanceCachingFactory(domainRepositoryFactory) }, + token: SERVICES.HEALTHCHECK, + provider: { + useFactory: instanceCachingFactory((container) => { + const connection = container.resolve(Pool); + return healthCheck(connection); + }), + }, }, { 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..7f92e9c6 100644 --- a/apps/auth-manager/src/domain/DAL/domainRepository.ts +++ b/apps/auth-manager/src/domain/DAL/domainRepository.ts @@ -1,26 +1,16 @@ -import { Domain } from '@map-colonies/auth-core'; -import type { FactoryFunction } from 'tsyringe'; -import type { Repository } from 'typeorm'; -import { DataSource } from 'typeorm'; +import { inArray } from 'drizzle-orm'; +import { domainTable, type Drizzle } from '@map-colonies/auth-core'; +import { inject, Lifecycle, scoped } from 'tsyringe'; +import { SERVICES } from '@common/constants'; -export type DomainRepository = Repository & { - checkInputForNonExistingDomains: (domainNames: string[]) => Promise; -}; +@scoped(Lifecycle.ContainerScoped) +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); - }, - }); -}; + const existingNames = new Set(existing.map((d) => d.name)); + return domainNames.filter((name) => !existingNames.has(name)); + } +} diff --git a/apps/auth-manager/src/domain/controllers/domainController.ts b/apps/auth-manager/src/domain/controllers/domainController.ts index e432fde1..f74b96be 100644 --- a/apps/auth-manager/src/domain/controllers/domainController.ts +++ b/apps/auth-manager/src/domain/controllers/domainController.ts @@ -1,10 +1,9 @@ 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'; +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 67dc7f45..bc505741 100644 --- a/apps/auth-manager/src/domain/models/domainManager.ts +++ b/apps/auth-manager/src/domain/models/domainManager.ts @@ -1,44 +1,46 @@ -import { type Logger } from '@map-colonies/js-logger'; -import { Domain, 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 { PaginationParams, paginationParamsToFindOptions } from '@src/common/db/pagination'; -import { SortOptions } from '@src/common/db/sort'; +import { count } from 'drizzle-orm'; +import { isDrizzleQueryError } from '@map-colonies/drizzle-utils'; +import { type PaginationParams, paginationParamsToOffsetAndLimit, type SortOptions } from '@map-colonies/drizzle-utils'; import { SERVICES } from '@common/constants'; -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: 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.domainRepository.findAndCount(findOptions); + 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]; } - 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); + 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 97cf1d8b..62d8ae59 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, 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..2c9a9406 100644 --- a/apps/auth-manager/src/key/models/keyManager.ts +++ b/apps/auth-manager/src/key/models/keyManager.ts @@ -1,45 +1,43 @@ 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.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.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 +47,11 @@ 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); - const maxVersion = await transactionRepo.getMaxVersionWithLock(key.environment); + return this.drizzle.transaction(async (tx) => { + const maxVersion = await this.keyRepository.getMaxVersionWithLock(key.environment, tx); if (maxVersion === null) { if (key.version !== 1) { @@ -64,7 +61,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 +73,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/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/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/configurations/vitest.globalSetup.mts b/apps/auth-manager/tests/configurations/vitest.globalSetup.mts index 8ca46e44..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'; @@ -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 521569e9..c48ff2bd 100644 --- a/apps/auth-manager/tests/integration/asset/asset.spec.mts +++ b/apps/auth-manager/tests/integration/asset/asset.spec.mts @@ -1,58 +1,40 @@ /// -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 { Asset, 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; - }); - - afterAll(async function () { - await depContainer.resolve(DataSource).destroy(); + const env = await initEnvironment(); + depContainer = env.container; + requestSender = env.requestSender; + drizzle = env.drizzle; }); 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(Asset).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const res = await requestSender.getAssets(); @@ -63,39 +45,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(Asset).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(Asset).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 +83,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(Asset).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(Asset).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(Asset).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(Asset).save(assets); + await drizzle.insert(assetTable).values(assets).execute(); const expectedAsset = assets.find((a) => a.version === 3); @@ -171,19 +148,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(Asset).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 +170,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 +183,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(Asset).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 +253,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 +266,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 +278,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 +291,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 +304,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..ab2a6fe1 100644 --- a/apps/auth-manager/tests/integration/bundle/bundle.spec.mts +++ b/apps/auth-manager/tests/integration/bundle/bundle.spec.mts @@ -1,47 +1,35 @@ /// -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]; }); describe('Happy Path', function () { @@ -63,8 +51,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 +70,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 +114,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 +124,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..dbb528e5 100644 --- a/apps/auth-manager/tests/integration/client/client.spec.mts +++ b/apps/auth-manager/tests/integration/client/client.spec.mts @@ -1,63 +1,45 @@ /// -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 }))); + 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 +51,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: { @@ -86,8 +67,7 @@ describe('client', function () { 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 +75,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 +86,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 +95,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 +110,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,16 +120,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(PAGE_SIZE); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); }); 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; @@ -163,8 +136,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,16 +146,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(PAGE_SIZE); - // @ts-expect-error need to solve as openapi-helpers is not typed correctly expect(res.body.total).toBe(TOTAL_CLIENTS); }); 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; @@ -194,8 +163,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 +173,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 +188,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 +196,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,32 +216,27 @@ 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]); } }); 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) => ({ @@ -289,8 +245,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 +253,19 @@ 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 +276,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 +285,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 +295,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'; @@ -364,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; @@ -374,8 +320,7 @@ 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({ queryParams: { @@ -385,16 +330,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 +351,7 @@ 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 } }); @@ -423,12 +365,15 @@ 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({ 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 +386,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 +397,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 +408,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 +434,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 +455,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 +468,37 @@ 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) }); + 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 () { + 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 () { + 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..f675c69e 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 () { @@ -85,10 +77,12 @@ describe('connection', function () { { 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,21 +98,19 @@ 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({ queryParams: { - name: 'avi', + name: 'rofl', }, }); - 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).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] }); + 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); }); }); @@ -319,23 +312,23 @@ 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) }); }); }); 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,7 +340,8 @@ 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) }); }); }); }); @@ -355,8 +349,7 @@ describe('connection', function () { 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 = []; + const connection = getFakeConnection(false, { name: clients[0]!.name, domains: [] }); const res = await requestSender.upsertConnection({ requestBody: connection }); expect(res).toHaveProperty('status', httpStatusCodes.BAD_REQUEST); @@ -364,7 +357,7 @@ describe('connection', function () { }); 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,7 +366,7 @@ 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 }); @@ -382,8 +375,8 @@ describe('connection', function () { }); 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 +393,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 +480,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 +497,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,10 +510,10 @@ 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 }); @@ -530,12 +525,7 @@ 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); + vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getClientConnections({ pathParams: { clientName: 'avi' } }); @@ -546,12 +536,7 @@ 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); + vi.spyOn(drizzle, 'select').mockRejectedValue(new Error('DB Error')); const res = await requestSender.getClientEnvironmentConnections({ pathParams: { clientName: 'avi', environment: Environment.NP } }); @@ -562,13 +547,12 @@ 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()); + 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 +565,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/docs/docs.spec.ts b/apps/auth-manager/tests/integration/docs/docs.spec.ts index fb2ee949..918a72dc 100644 --- a/apps/auth-manager/tests/integration/docs/docs.spec.ts +++ b/apps/auth-manager/tests/integration/docs/docs.spec.ts @@ -3,11 +3,10 @@ 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 { getApp } from '../../../src/app'; -import { initConfig } from '../../../src/common/config'; -import { SERVICES } from '../../../src/common/constants'; +import { Pool } from 'pg'; +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 () { @@ -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 fad73e13..d4ccaafd 100644 --- a/apps/auth-manager/tests/integration/domain/domain.spec.mts +++ b/apps/auth-manager/tests/integration/domain/domain.spec.mts @@ -1,56 +1,43 @@ -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 { 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 type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { createRequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { Pool } from 'pg'; +import type { Drizzle } from '@map-colonies/auth-core'; +import { domainTable } from '@map-colonies/auth-core'; +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(DataSource).destroy(); + 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 connection = depContainer.resolve(DataSource); - await connection.getRepository(Domain).save([{ name: 'avi' }, { name: 'iva' }]); + 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' }])); }); @@ -108,7 +95,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 +103,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/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/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/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts b/apps/auth-manager/tests/unit/asset/models/assetManager.spec.mts index ebe61871..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,154 +1,75 @@ 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 type { Drizzle } from '@map-colonies/auth-core'; 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 }); 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('#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 = { + 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); - }); - - 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); + 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/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..08bcfcab 100644 --- a/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts +++ b/apps/auth-manager/tests/unit/client/models/clientManager.spec.mts @@ -1,138 +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 { pgErrorCodes } from '@map-colonies/drizzle-utils'; +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'; 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(); - }); - - 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); - }); + mockValues.mockReturnValue({ returning: mockReturning }); + drizzle.insert.mockReturnValue({ values: mockValues }); + clientManager = new ClientManager(logger, drizzle as unknown as Drizzle); }); 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'); - 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(); - }); - - 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(); + 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 b5f7652f..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,252 +1,82 @@ 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('#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 = { + 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 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; - 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); - }); - - 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', ''); + 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); 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/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/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/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-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..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:", - "typeorm": "catalog:" + "drizzle-orm": "catalog:", + "@map-colonies/drizzle-utils": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", @@ -58,8 +59,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 53a89991..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 { Asset, 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(Asset); - 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(Asset) - .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..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); 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/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.ts b/packages/auth-bundler/tests/db.spec.mts similarity index 51% rename from packages/auth-bundler/tests/db.spec.ts rename to packages/auth-bundler/tests/db.spec.mts index 8da4c92b..cafb6a18 100644 --- a/packages/auth-bundler/tests/db.spec.ts +++ b/packages/auth-bundler/tests/db.spec.mts @@ -1,15 +1,13 @@ /// -import { Asset, Bundle, Connection, Environment, Key, createConnectionOptions } from '@map-colonies/auth-core'; -import { DataSource } from 'typeorm'; +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 { 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 +20,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(Asset).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 +55,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 +76,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 +94,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(Asset).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 +108,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/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/drizzle.config.mts b/packages/auth-core/drizzle.config.mts new file mode 100644 index 00000000..50a7192d --- /dev/null +++ b/packages/auth-core/drizzle.config.mts @@ -0,0 +1,26 @@ +import { ConnectionConfig } from 'pg'; +import { defineConfig } from 'drizzle-kit'; +import { getConfig, initConfig } from './src/config.js'; + +import { createConnectionOptions } from './src/db/utils/createConnection.js'; +import { ConnectionOptions } from 'node:tls'; + +await initConfig(); + +const config = getConfig().getAll(); + +const dbOptions = createConnectionOptions(config) as Omit, 'password' | 'ssl'> & { + password: string; + ssl?: ConnectionOptions; +}; + +export default defineConfig({ + schema: ['./src/entities/index.ts'], + out: './src/migrations', + dialect: 'postgresql', + schemaFilter: ['auth_manager', 'public'], + dbCredentials: { ...dbOptions, user: dbOptions.user, ssl: false }, + verbose: true, + migrations: { schema: config.schema }, + strict: false, +}); diff --git a/packages/auth-core/eslint.config.mjs b/packages/auth-core/eslint.config.mjs index 5338ece2..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.ts', 'vitest.config.mts', '**/migrations/**'])); +export default defineConfig(tsBaseConfig, globalIgnores(['drizzle.config.mts', 'vitest.config.mts', '**/migrations/**', 'runMigrations.mts'])); diff --git a/packages/auth-core/package.json b/packages/auth-core/package.json index e0ae3d20..e220e145 100644 --- a/packages/auth-core/package.json +++ b/packages/auth-core/package.json @@ -13,20 +13,21 @@ }, "exports": { ".": { - "import": "./dist/index.js", + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, "require": "./dist/index.js" } }, "scripts": { - "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 -- ", + "migration:create": "drizzle-kit generate --config drizzle.config.mts", + "migration:run": "pnpm run build && node runMigrations.mts", "lint": "eslint .", "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 2 src/migrations/**/* dist/migrations", "clean": "rimraf dist", "prepack": "turbo run build", "check-dist": "publint && attw --pack .", @@ -37,19 +38,20 @@ }, "dependencies": { "pg": "catalog:", - "typeorm": "catalog:" + "@map-colonies/schemas": "catalog:", + "@map-colonies/drizzle-utils": "catalog:" }, "devDependencies": { "@map-colonies/config": "catalog:", - "@map-colonies/schemas": "catalog:", "@map-colonies/tsconfig": "catalog:", "@map-colonies/eslint-config": "catalog:", "@types/node": "catalog:", "@types/pg": "catalog:", - "ts-node": "^10.9.1", - "typescript": "catalog:" + "typescript": "catalog:", + "drizzle-kit": "catalog:" }, "peerDependencies": { - "@map-colonies/js-logger": "catalog:" + "@map-colonies/js-logger": "catalog:", + "drizzle-orm": "catalog:" } } diff --git a/packages/auth-core/runMigrations.mts b/packages/auth-core/runMigrations.mts new file mode 100644 index 00000000..aecdb1f4 --- /dev/null +++ b/packages/auth-core/runMigrations.mts @@ -0,0 +1,21 @@ +import { initConnection } from '@map-colonies/drizzle-utils'; +import { config } from '@map-colonies/config'; +import schemas from '@map-colonies/schemas'; +import { createDrizzle, runMigrations } from './dist/drizzle.js'; + +const { commonDbFullV2 } = schemas; + +(async () => { + 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/auth-core/src/db/entities/asset.ts b/packages/auth-core/src/db/entities/asset.ts deleted file mode 100644 index 43fb1da4..00000000 --- a/packages/auth-core/src/db/entities/asset.ts +++ /dev/null @@ -1,43 +0,0 @@ -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; -} 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 1f31ac48..00000000 --- a/packages/auth-core/src/db/entities/bundle.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; -} 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 5f80ea0b..00000000 --- a/packages/auth-core/src/db/entities/client.ts +++ /dev/null @@ -1,35 +0,0 @@ -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; -} 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 3c186e92..00000000 --- a/packages/auth-core/src/db/entities/connection.ts +++ /dev/null @@ -1,38 +0,0 @@ -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; -} 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 02e4c77d..00000000 --- a/packages/auth-core/src/db/entities/domain.ts +++ /dev/null @@ -1,11 +0,0 @@ -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; -} diff --git a/packages/auth-core/src/db/entities/index.ts b/packages/auth-core/src/db/entities/index.ts deleted file mode 100644 index 037b453a..00000000 --- a/packages/auth-core/src/db/entities/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './asset'; -export * from './bundle'; -export * from './client'; -export * from './connection'; -export * from './domain'; -export * from './key'; 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 88f3a7cf..00000000 --- a/packages/auth-core/src/db/entities/key.ts +++ /dev/null @@ -1,20 +0,0 @@ -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; - - @PrimaryColumn({ type: 'integer' }) - public version!: number; - - @Column({ type: 'jsonb', name: 'private_key' }) - public privateKey!: JWKPrivateKey; - - @Column({ type: 'jsonb', name: 'public_key' }) - public publicKey!: JWKPublicKey; -} diff --git a/packages/auth-core/src/db/index.ts b/packages/auth-core/src/db/index.ts deleted file mode 100644 index 95967516..00000000 --- a/packages/auth-core/src/db/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -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/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/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 6c707815..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 f8a68c6c..00000000 --- a/packages/auth-core/src/db/utils/createConnection.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { hostname } from 'node:os'; -import { readFileSync } from 'node:fs'; -import type { TlsOptions } from 'node:tls'; -import { DataSource, type DataSourceOptions } from 'typeorm'; -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) }; - } - - 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; -}; 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/entities/asset.ts b/packages/auth-core/src/entities/asset.ts new file mode 100644 index 00000000..ad3a3930 --- /dev/null +++ b/packages/auth-core/src/entities/asset.ts @@ -0,0 +1,35 @@ +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( + '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' })] +); + +/* 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/entities/common.ts b/packages/auth-core/src/entities/common.ts new file mode 100644 index 00000000..161769b4 --- /dev/null +++ b/packages/auth-core/src/entities/common.ts @@ -0,0 +1,21 @@ +import * as d from 'drizzle-orm/pg-core'; +import { timestamp } from 'drizzle-orm/pg-core'; + +type EnvironmentValues = typeof environmentEnum.enumValues; + +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. */ + 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 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/entities/index.ts b/packages/auth-core/src/entities/index.ts new file mode 100644 index 00000000..2a9b0504 --- /dev/null +++ b/packages/auth-core/src/entities/index.ts @@ -0,0 +1,25 @@ +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'; +export * from './connection'; +export * from './domain'; +export * from './key'; +export * from './common'; 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/migrations/20260513061159_curious_speedball/migration.sql b/packages/auth-core/src/migrations/20260513061159_curious_speedball/migration.sql new file mode 100644 index 00000000..772bca8c --- /dev/null +++ b/packages/auth-core/src/migrations/20260513061159_curious_speedball/migration.sql @@ -0,0 +1,65 @@ +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 +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/migrations/20260513061159_curious_speedball/snapshot.json b/packages/auth-core/src/migrations/20260513061159_curious_speedball/snapshot.json new file mode 100644 index 00000000..3960d72c --- /dev/null +++ b/packages/auth-core/src/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/model/asset.ts b/packages/auth-core/src/model/asset.ts deleted file mode 100644 index 43f4c296..00000000 --- a/packages/auth-core/src/model/asset.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 */ - -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; -} diff --git a/packages/auth-core/src/model/bundle.ts b/packages/auth-core/src/model/bundle.ts deleted file mode 100644 index d6a1d38e..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 0d2f3fe0..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/common.ts b/packages/auth-core/src/model/common.ts deleted file mode 100644 index daec024d..00000000 --- a/packages/auth-core/src/model/common.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** 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 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 deleted file mode 100644 index 5bc0ddab..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 808cb42e..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 61b77d27..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/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 diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index c3bca6c8..66ad46bf 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": "catalog:", + "drizzle-orm": "catalog:", + "pg": "catalog:" }, "peerDependencies": { "vitest": "catalog:" @@ -47,8 +48,9 @@ "@map-colonies/eslint-config": "catalog:", "@map-colonies/tsconfig": "catalog:", "@types/node": "catalog:", + "@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 new file mode 100644 index 00000000..8f331eef --- /dev/null +++ b/packages/test-utils/src/drizzle.ts @@ -0,0 +1,15 @@ +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`); + + const db = createDrizzle(connection); + await runMigrations(db); +} + +export type { commonDbFullV1Type as TypeormConnectionOptions }; diff --git a/packages/test-utils/src/fakers.ts b/packages/test-utils/src/fakers.ts index cab27930..d7c15308 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, 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], @@ -13,14 +26,17 @@ 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, + ...override, }; } -export function getFakeConnection(): Connection { - return { - createdAt: faker.date.past(), +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, name: faker.string.alpha(EIGHT), @@ -31,15 +47,12 @@ export function getFakeConnection(): Connection { enabled: true, token: faker.string.alpha(), }; + return { ...connection, ...override }; } -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 +66,12 @@ export function getFakeBundle(includeCreated?: boolean): IBundle { }; } -export function getFakeClient(includeGeneratedFields: boolean): IClient { +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: IClient = { + const client: NewClient = { name: faker.internet.username({ firstName, lastName }), hebName: 'אבי', branch: faker.company.buzzVerb(), @@ -74,11 +89,11 @@ 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() }); } - return client; + return { ...client, ...override }; } export function getMockKeys(): [JWKPrivateKey, JWKPublicKey] { 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 435c70c2..fc59a7b3 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: 0.2.0 + version: 0.2.0 '@map-colonies/error-express-handler': specifier: ^4.0.0 version: 4.0.0 @@ -100,11 +103,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 @@ -126,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 @@ -150,9 +156,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 @@ -248,6 +251,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: 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) @@ -266,6 +272,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,13 +287,10 @@ 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:' - 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 @@ -300,6 +306,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) @@ -312,9 +321,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) typescript: specifier: 'catalog:' version: 5.9.3 @@ -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: 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 @@ -378,6 +387,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,16 +414,13 @@ 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:' 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) @@ -463,12 +472,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 typescript: specifier: 'catalog:' version: 5.9.3 @@ -533,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 @@ -741,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: 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 @@ -785,7 +791,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 @@ -840,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) @@ -888,7 +894,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 @@ -901,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 @@ -919,9 +922,15 @@ importers: '@map-colonies/auth-core': specifier: workspace:^ version: link:../auth-core + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + 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) + 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 @@ -931,19 +940,13 @@ 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) '@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 @@ -983,25 +986,28 @@ importers: packages/auth-core: dependencies: + '@map-colonies/drizzle-utils': + specifier: 'catalog:' + 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) + '@map-colonies/schemas': + specifier: 'catalog:' + version: 1.21.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) 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:' 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) - '@map-colonies/schemas': - specifier: 'catalog:' - version: 1.21.0 + 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 @@ -1011,9 +1017,9 @@ importers: '@types/pg': specifier: 'catalog:' version: 8.20.0 - ts-node: - specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.15.21)(@types/node@24.12.0)(typescript@5.9.3) + drizzle-kit: + specifier: 'catalog:' + version: 1.0.0-rc.2 typescript: specifier: 'catalog:' version: 5.9.3 @@ -1022,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) @@ -1050,28 +1056,34 @@ 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: 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: 4.17.23 + pg: + specifier: 'catalog:' + 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)) 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:' version: 24.12.0 + '@types/pg': + specifier: 'catalog:' + version: 8.20.0 eslint: specifier: 'catalog:' version: 9.39.4(jiti@2.6.1) @@ -1083,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) @@ -1750,8 +1762,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==} @@ -1771,14 +1783,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'} @@ -1791,12 +1795,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'} @@ -1809,12 +1807,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'} @@ -1827,12 +1819,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'} @@ -1845,12 +1831,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'} @@ -1863,12 +1843,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'} @@ -1881,12 +1855,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'} @@ -1899,12 +1867,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'} @@ -1917,12 +1879,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'} @@ -1935,12 +1891,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'} @@ -1953,12 +1903,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'} @@ -1971,12 +1915,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'} @@ -1989,12 +1927,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'} @@ -2007,12 +1939,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'} @@ -2025,12 +1951,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'} @@ -2043,12 +1963,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'} @@ -2061,12 +1975,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'} @@ -2091,12 +1999,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'} @@ -2121,12 +2023,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'} @@ -2151,12 +2047,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'} @@ -2169,12 +2059,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'} @@ -2187,12 +2071,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'} @@ -2205,12 +2083,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'} @@ -2458,6 +2330,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==} @@ -2493,6 +2369,13 @@ packages: prom-client: optional: true + '@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 + pg: ^8.20.0 + '@map-colonies/error-express-handler@4.0.0': resolution: {integrity: sha512-3vDxDN0IlBy6gRBIyh5/fiKRQeJV2DZDJDEckPoBCADtWSdtzKhM+F+c3HdSru/8tm4lPYeDS9JVeOPJk419Ag==} engines: {node: '>=24'} @@ -4245,9 +4128,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==} @@ -5083,10 +4963,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==} @@ -5094,10 +4970,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==} @@ -5749,9 +5621,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: @@ -5887,19 +5756,16 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dotenv@16.6.1: - 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' @@ -5907,31 +5773,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': @@ -5946,12 +5821,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': @@ -5962,17 +5847,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 @@ -5980,12 +5865,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==} @@ -6075,11 +5964,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'} @@ -7180,6 +7064,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'} @@ -8639,11 +8526,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==} @@ -8765,10 +8647,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==} @@ -9052,10 +8930,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'} @@ -9195,61 +9069,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} @@ -9393,16 +9212,14 @@ 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). 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: @@ -9698,9 +9515,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==} @@ -10808,7 +10622,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: @@ -10834,160 +10648,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 @@ -11000,9 +10756,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 @@ -11015,9 +10768,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 @@ -11030,36 +10780,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 @@ -11430,6 +11168,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)': @@ -11475,11 +11217,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@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 + 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 - '@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) @@ -11492,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: @@ -13440,8 +13189,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': {} @@ -14441,8 +14188,6 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.2.0: {} - any-promise@1.3.0: {} anymatch@3.1.3: @@ -14450,8 +14195,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: @@ -15187,8 +14930,6 @@ snapshots: dateformat@4.6.3: {} - dayjs@1.11.20: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -15205,7 +14946,8 @@ snapshots: dependencies: mimic-response: 3.1.0 - dedent@1.7.2: {} + dedent@1.7.2: + optional: true deep-is@0.1.4: {} @@ -15301,20 +15043,21 @@ snapshots: dotenv@16.4.7: {} - 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: @@ -15447,31 +15190,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 @@ -15585,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) @@ -17009,6 +16727,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsbi@4.3.2: {} + jsep@1.4.0: {} jsesc@3.1.0: {} @@ -17078,7 +16798,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' @@ -18579,12 +18299,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: @@ -18723,8 +18437,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 @@ -19081,12 +18793,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 @@ -19159,6 +18865,7 @@ snapshots: get-tsconfig: 4.13.7 optionalDependencies: fsevents: 2.3.3 + optional: true tsyringe@4.10.0: dependencies: @@ -19252,30 +18959,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) @@ -19413,8 +19096,6 @@ snapshots: uuid@10.0.0: {} - uuid@11.1.0: {} - uuid@8.3.2: {} uuid@9.0.1: {} @@ -19662,6 +19343,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..3626d866 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' @@ -26,7 +27,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' @@ -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' @@ -45,10 +45,11 @@ 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' - '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' diff --git a/turbo.json b/turbo.json index b723c00e..a4f8b009 100644 --- a/turbo.json +++ b/turbo.json @@ -1,7 +1,8 @@ { "$schema": "https://turborepo.com/schema.json", "ui": "stream", - "globalDependencies": ["turbo.json", "pnpm-workspace.yaml", "vitest.config.ts"], + "concurrency": "4", + "globalDependencies": ["turbo.json", "pnpm-workspace.yaml"], "tasks": { "build": { "dependsOn": ["^build"],