diff --git a/apps/cli/package.json b/apps/cli/package.json index 8e4d6007..3f28c12b 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -6,7 +6,7 @@ "license": "CC0", "main": "src/index.ts", "scripts": { - "build": "node ./build.js", + "build": "tsup src/* --format cjs", "cli": "node dist/index.js", "dev": "tsup src/* --watch", "test": "vitest run --coverage" diff --git a/apps/server-doj/build.js b/apps/server-doj/build.js new file mode 100644 index 00000000..aaec3737 --- /dev/null +++ b/apps/server-doj/build.js @@ -0,0 +1,14 @@ +import esbuild from 'esbuild'; + +esbuild + .build({ + bundle: true, + entryPoints: ['./src/index.ts'], + format: 'esm', + minify: true, + outdir: './dist', + platform: 'node', + sourcemap: true, + target: 'esnext', + }) + .catch(() => process.exit(1)); diff --git a/apps/server-doj/src/index.ts b/apps/server-doj/src/index.ts index f5d64a6e..edca57e5 100644 --- a/apps/server-doj/src/index.ts +++ b/apps/server-doj/src/index.ts @@ -1,8 +1,21 @@ import { createCustomServer } from './server.js'; const port = process.env.PORT || 4321; -const app = await createCustomServer(); -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); -}); +const getCloudGovServerSecrets = () => { + if (process.env.VCAP_SERVICES === undefined) { + throw new Error('VCAP_SERVICES not found'); + } + const services = JSON.parse(process.env.VCAP_SERVICES || '{}'); + return { + //loginGovClientSecret: services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY, + dbUri: services['aws-rds'][0].credentials.uri as string, + }; +}; + +const secrets = getCloudGovServerSecrets(); +createCustomServer({ dbUri: secrets?.dbUri }).then((server: any) => + server.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); + }) +); diff --git a/apps/server-doj/src/server.ts b/apps/server-doj/src/server.ts index 62837720..943ab0a6 100644 --- a/apps/server-doj/src/server.ts +++ b/apps/server-doj/src/server.ts @@ -1,18 +1,15 @@ -import path, { dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { + createDatabaseGateway, + createPostgresDatabaseContext, +} from '@atj/database'; +import { createServer } from '@atj/server'; -const getDirname = () => dirname(fileURLToPath(import.meta.url)); - -export const createCustomServer = async (): Promise => { - const { createDevDatabaseContext, createDatabaseGateway } = await import( - '@atj/database' - ); - const { createServer } = await import('@atj/server'); - - const dbCtx = await createDevDatabaseContext( - path.join(getDirname(), '../doj.db') +export const createCustomServer = async (ctx: { + dbUri: string; +}): Promise => { + const db = createDatabaseGateway( + await createPostgresDatabaseContext(ctx.dbUri, true) ); - const db = createDatabaseGateway(dbCtx); return createServer({ title: 'DOJ Form Service', @@ -23,16 +20,18 @@ export const createCustomServer = async (): Promise => { 'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:tts-10x-atj-dev-server-doj', //clientSecret: '', // secrets.loginGovClientSecret, }, + isUserAuthorized: async (email: string) => { + return [ + // 10x team members + 'daniel.naab@gsa.gov', + 'jim.moffet@gsa.gov', + 'ethan.gardner@gsa.gov', + 'natasha.pierre-louis@gsa.gov', + 'emily.lordahl@gsa.gov', + // DOJ test users + 'deserene.h.worsley@usdoj.gov', + 'jordan.pendergrass@usdoj.gov', + ].includes(email); + }, }); }; - -/* -const getServerSecrets = () => { - const services = JSON.parse(process.env.VCAP_SERVICES || '{}'); - const loginClientSecret = - services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY; - return { - loginGovClientSecret: loginClientSecret, - }; -}; -*/ diff --git a/apps/server-doj/tests/integration.test.ts b/apps/server-doj/tests/integration.test.ts index 1d535e0d..73d8b3aa 100644 --- a/apps/server-doj/tests/integration.test.ts +++ b/apps/server-doj/tests/integration.test.ts @@ -1,16 +1,18 @@ import request from 'supertest'; -import { beforeAll, describe, expect, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; +import { describeDatabase } from '@atj/database/testing'; import { createCustomServer } from '../src/server'; describe('DOJ Form Service', () => { - let app: any; - - beforeAll(async () => { - app = await createCustomServer(); + test('avoid "No test suite found in file" error', async () => { + expect(true).toBe(true); }); +}); - test('renders the home page', async () => { +describeDatabase('DOJ Form Service', () => { + test('renders the home page', async ({ db }) => { + const app = await createCustomServer({ dbUri: db.ctx.connectionUri }); const response = await request(app).get('/'); expect(response.ok).toBe(true); expect(response.text).toMatch(/DOJ Form Service/); diff --git a/apps/server-doj/tsconfig.json b/apps/server-doj/tsconfig.json index a7c700ff..ed4cddf1 100644 --- a/apps/server-doj/tsconfig.json +++ b/apps/server-doj/tsconfig.json @@ -1,12 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "module": "ESNext", - "emitDeclarationOnly": true, + "module": "CommonJS", + "moduleResolution": "NodeNext", "outDir": "./dist", - "target": "es2022" + "emitDeclarationOnly": true }, - "include": ["./src/**/*"], - "exclude": ["./dist"], + "include": ["./src"], "references": [] } diff --git a/apps/server-doj/vitest.config.ts b/apps/server-doj/vitest.config.ts new file mode 100644 index 00000000..55f8f580 --- /dev/null +++ b/apps/server-doj/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; +import { getDatabaseTestContainerGlobalSetupPath } from '@atj/database'; + +import sharedTestConfig from '../../vitest.shared'; + +export default defineConfig({ + ...sharedTestConfig, + test: { + ...sharedTestConfig.test, + globalSetup: [getDatabaseTestContainerGlobalSetupPath()], + }, +}); diff --git a/apps/server-kansas/src/index.ts b/apps/server-kansas/src/index.ts index 82e7c8ce..edca57e5 100644 --- a/apps/server-kansas/src/index.ts +++ b/apps/server-kansas/src/index.ts @@ -1,8 +1,21 @@ -import { createCustomServer } from './server'; +import { createCustomServer } from './server.js'; const port = process.env.PORT || 4321; -const app = await createCustomServer(); -app.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); -}); +const getCloudGovServerSecrets = () => { + if (process.env.VCAP_SERVICES === undefined) { + throw new Error('VCAP_SERVICES not found'); + } + const services = JSON.parse(process.env.VCAP_SERVICES || '{}'); + return { + //loginGovClientSecret: services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY, + dbUri: services['aws-rds'][0].credentials.uri as string, + }; +}; + +const secrets = getCloudGovServerSecrets(); +createCustomServer({ dbUri: secrets?.dbUri }).then((server: any) => + server.listen(port, () => { + console.log(`Server running on http://localhost:${port}`); + }) +); diff --git a/apps/server-kansas/src/server.ts b/apps/server-kansas/src/server.ts index d8249180..908d40f9 100644 --- a/apps/server-kansas/src/server.ts +++ b/apps/server-kansas/src/server.ts @@ -1,21 +1,18 @@ -import path, { dirname } from 'path'; -import { fileURLToPath } from 'url'; +import { + createDatabaseGateway, + createPostgresDatabaseContext, +} from '@atj/database'; +import { createServer } from '@atj/server'; -const getDirname = () => dirname(fileURLToPath(import.meta.url)); - -export const createCustomServer = async (): Promise => { - const { createDevDatabaseContext, createDatabaseGateway } = await import( - '@atj/database' - ); - const { createServer } = await import('@atj/server'); - - const dbCtx = await createDevDatabaseContext( - path.join(getDirname(), '../doj.db') +export const createCustomServer = async (ctx: { + dbUri: string; +}): Promise => { + const db = createDatabaseGateway( + await createPostgresDatabaseContext(ctx.dbUri, true) ); - const db = createDatabaseGateway(dbCtx); return createServer({ - title: 'KS Courts Form Service', + title: 'DOJ Form Service', db, loginGovOptions: { loginGovUrl: 'https://idp.int.identitysandbox.gov', @@ -23,16 +20,14 @@ export const createCustomServer = async (): Promise => { 'urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:tts-10x-atj-dev-server-doj', //clientSecret: '', // secrets.loginGovClientSecret, }, + isUserAuthorized: async (email: string) => { + return [ + 'daniel.naab@gsa.gov', + 'jim.moffet@gsa.gov', + 'ethan.gardner@gsa.gov', + 'natasha.pierre-louis@gsa.gov', + 'emily.lordahl@gsa.gov', + ].includes(email); + }, }); }; - -/* -const getServerSecrets = () => { - const services = JSON.parse(process.env.VCAP_SERVICES || '{}'); - const loginClientSecret = - services['user-provided']?.credentials?.SECRET_LOGIN_GOV_PRIVATE_KEY; - return { - loginGovClientSecret: loginClientSecret, - }; -}; -*/ diff --git a/apps/server-kansas/tests/integration.test.ts b/apps/server-kansas/tests/integration.test.ts index 97e88fb1..f602bed0 100644 --- a/apps/server-kansas/tests/integration.test.ts +++ b/apps/server-kansas/tests/integration.test.ts @@ -1,18 +1,20 @@ import request from 'supertest'; -import { beforeAll, describe, expect, test } from 'vitest'; +import { describe, expect, test } from 'vitest'; +import { describeDatabase } from '@atj/database/testing'; import { createCustomServer } from '../src/server'; -describe('Kansas State Courts Form Service', () => { - let app: any; - - beforeAll(async () => { - app = await createCustomServer(); +describe('DOJ Form Service', () => { + test('avoid "No test suite found in file" error', async () => { + expect(true).toBe(true); }); +}); - test('renders the home page', async () => { +describeDatabase('Kansas State Courts Form Service', () => { + test('renders the home page', async ({ db }) => { + const app = await createCustomServer({ dbUri: db.ctx.connectionUri }); const response = await request(app).get('/'); expect(response.ok).toBe(true); - expect(response.text).toMatch(/KS Courts Form Service/); + expect(response.text).toMatch(/DOJ Form Service/); }); }); diff --git a/infra/cdktf/src/lib/cloud.gov/node-astro.ts b/infra/cdktf/src/lib/cloud.gov/node-astro.ts index a7ed8ed4..3c3915d1 100644 --- a/infra/cdktf/src/lib/cloud.gov/node-astro.ts +++ b/infra/cdktf/src/lib/cloud.gov/node-astro.ts @@ -41,6 +41,34 @@ export class AstroService extends Construct { } ); + const rds = + new cloudfoundry.dataCloudfoundryService.DataCloudfoundryService( + scope, + `${id}-data-aws-rds`, + { + name: 'aws-rds', + } + ); + + const dbInstance = new cloudfoundry.serviceInstance.ServiceInstance( + this, + `${id}-db`, + { + name: `${id}-db`, + servicePlan: rds.servicePlans.lookup('micro-psql'), + space: spaceId, + jsonParams: '{"version": "15"}', + lifecycle: { + preventDestroy: true, + }, + timeouts: { + create: '60m', + update: '60m', + delete: '2h', + }, + } + ); + new cloudfoundry.app.App(this, `${id}-app`, { name: `${id}-app`, space: spaceId, @@ -55,6 +83,9 @@ export class AstroService extends Construct { }, ], serviceBinding: [ + { + serviceInstance: dbInstance.id, + }, { serviceInstance: loginGovService.id, }, diff --git a/packages/auth/package.json b/packages/auth/package.json index af5046ca..0e86b3b8 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -14,6 +14,7 @@ "dependencies": { "@atj/common": "workspace:^", "@atj/database": "workspace:*", + "@lucia-auth/adapter-postgresql": "^3.1.2", "@lucia-auth/adapter-sqlite": "^3.0.2", "arctic": "^1.9.2", "better-sqlite3": "^11.1.2", diff --git a/packages/auth/src/context/dev.ts b/packages/auth/src/context/base.ts similarity index 57% rename from packages/auth/src/context/dev.ts rename to packages/auth/src/context/base.ts index 08f7e12d..786e2309 100644 --- a/packages/auth/src/context/dev.ts +++ b/packages/auth/src/context/base.ts @@ -3,10 +3,10 @@ import { Cookie, Lucia } from 'lucia'; import { type DatabaseGateway } from '@atj/database'; import { type AuthContext, type UserSession } from '..'; -import { createTestLuciaAdapter } from '../lucia'; +import { createPostgresLuciaAdapter, createSqliteLuciaAdapter } from '../lucia'; import { LoginGov } from '../provider'; -export class DevAuthContext implements AuthContext { +export class BaseAuthContext implements AuthContext { private lucia?: Lucia; constructor( @@ -14,13 +14,19 @@ export class DevAuthContext implements AuthContext { public provider: LoginGov, public getCookie: (name: string) => string | undefined, public setCookie: (cookie: Cookie) => void, - public setUserSession: (userSession: UserSession) => void + public setUserSession: (userSession: UserSession) => void, + public isUserAuthorized: (email: string) => Promise ) {} async getLucia() { - const sqlite3Adapter = createTestLuciaAdapter( - await (this.db.getContext() as any).getSqlite3() - ); + const sqlite3Adapter = + this.db.getContext().engine === 'sqlite' + ? createSqliteLuciaAdapter( + await (this.db.getContext() as any).getSqlite3() + ) + : createPostgresLuciaAdapter( + await (this.db.getContext() as any).getPostgresPool() + ); if (!this.lucia) { this.lucia = new Lucia(sqlite3Adapter, { sessionCookie: { diff --git a/packages/auth/src/context/test.ts b/packages/auth/src/context/test.ts index d6f22691..6fc632f1 100644 --- a/packages/auth/src/context/test.ts +++ b/packages/auth/src/context/test.ts @@ -3,18 +3,19 @@ import { vi } from 'vitest'; import { type DatabaseGateway, - createTestDatabaseContext, + createInMemoryDatabaseContext, createDatabaseGateway, } from '@atj/database'; import { type AuthContext, type UserSession } from '..'; -import { createTestLuciaAdapter } from '../lucia'; +import { createSqliteLuciaAdapter } from '../lucia'; import { LoginGov } from '../provider'; type Options = { getCookie: (name: string) => string | undefined; setCookie: (cookie: Cookie) => void; setUserSession: (userSession: UserSession) => void; + isUserAuthorized: (email: string) => Promise; }; export const createTestAuthContext = async (opts?: Partial) => { @@ -22,8 +23,9 @@ export const createTestAuthContext = async (opts?: Partial) => { getCookie: opts?.getCookie || vi.fn(), setCookie: opts?.setCookie || vi.fn(), setUserSession: opts?.setUserSession || vi.fn(), + isUserAuthorized: opts?.isUserAuthorized || vi.fn(async () => true), }; - const dbContext = await createTestDatabaseContext(); + const dbContext = await createInMemoryDatabaseContext(); const database = createDatabaseGateway(dbContext); return new TestAuthContext( database, @@ -36,7 +38,8 @@ export const createTestAuthContext = async (opts?: Partial) => { }), options.getCookie, options.setCookie, - options.setUserSession + options.setUserSession, + options.isUserAuthorized ); }; @@ -48,12 +51,13 @@ export class TestAuthContext implements AuthContext { public provider: LoginGov, public getCookie: (name: string) => string | undefined, public setCookie: (cookie: Cookie) => void, - public setUserSession: (userSession: UserSession) => void + public setUserSession: (userSession: UserSession) => void, + public isUserAuthorized: (email: string) => Promise ) {} async getLucia() { const sqlite3 = await (this.db.getContext() as any).getSqlite3(); - const sqlite3Adapter = createTestLuciaAdapter(sqlite3); + const sqlite3Adapter = createSqliteLuciaAdapter(sqlite3); if (!this.lucia) { this.lucia = new Lucia(sqlite3Adapter, { sessionCookie: { diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 9e1d6201..590efe8f 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -2,7 +2,7 @@ import { type Cookie, type User, type Session, type Lucia } from 'lucia'; import { type DatabaseGateway } from '@atj/database'; -export { DevAuthContext } from './context/dev'; +export { BaseAuthContext } from './context/base'; import { type LoginGovOptions, LoginGov } from './provider'; export { type LoginGovOptions, LoginGov }; export { getProviderRedirect } from './services/get-provider-redirect'; @@ -23,4 +23,5 @@ export type AuthContext = { setCookie: (cookie: Cookie) => void; setUserSession: (userSession: UserSession) => void; getLucia: () => Promise; + isUserAuthorized: (email: string) => Promise; }; diff --git a/packages/auth/src/lucia.ts b/packages/auth/src/lucia.ts index 4909a9c4..4761306d 100644 --- a/packages/auth/src/lucia.ts +++ b/packages/auth/src/lucia.ts @@ -1,15 +1,23 @@ +import { NodePostgresAdapter } from '@lucia-auth/adapter-postgresql'; import { BetterSqlite3Adapter } from '@lucia-auth/adapter-sqlite'; import { type Database as Sqlite3Database } from 'better-sqlite3'; import { Lucia } from 'lucia'; import { type Database } from '@atj/database'; -export const createTestLuciaAdapter = (db: Sqlite3Database) => { +export const createSqliteLuciaAdapter = (db: Sqlite3Database) => { const adapter = new BetterSqlite3Adapter(db, { user: 'users', session: 'sessions', }); - //const adapter = new KyselyAdapter(); + return adapter; +}; + +export const createPostgresLuciaAdapter = (pgPool: any) => { + const adapter = new NodePostgresAdapter(pgPool, { + user: 'users', + session: 'sessions', + }); return adapter; }; diff --git a/packages/auth/src/services/process-provider-callback.test.ts b/packages/auth/src/services/process-provider-callback.test.ts index 2d129f63..5c3b0469 100644 --- a/packages/auth/src/services/process-provider-callback.test.ts +++ b/packages/auth/src/services/process-provider-callback.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { processProviderCallback } from './process-provider-callback'; import { createTestAuthContext } from '../context/test'; import { AuthContext } from '..'; +import { success } from '@atj/common'; describe('processProviderCallback', () => { let ctx: AuthContext; @@ -101,4 +102,29 @@ describe('processProviderCallback', () => { }, }); }); + + it('fails with unauthorized email', async () => { + const ctx = await createTestAuthContext({ + isUserAuthorized: async () => false, + }); + const result = await processProviderCallback( + ctx, + { + code: 'params-code', + state: 'params-state', + }, + { + code: 'params-code', + state: 'params-state', + nonce: 't0j5AY5k4oLAcxfaZdRsMfVZGBCgBjlfihgoVq34YGo', + } + ); + expect(result).toEqual({ + error: { + message: 'permission denied', + status: 403, + }, + success: false, + }); + }); }); diff --git a/packages/auth/src/services/process-provider-callback.ts b/packages/auth/src/services/process-provider-callback.ts index 4b983697..b4ba5919 100644 --- a/packages/auth/src/services/process-provider-callback.ts +++ b/packages/auth/src/services/process-provider-callback.ts @@ -67,6 +67,10 @@ export const processProviderCallback = async ( if (!userDataResult.success) { return userDataResult; } + const isAuthorized = await ctx.isUserAuthorized(userDataResult.data.email); + if (!isAuthorized) { + return r.failure({ status: 403, message: 'permission denied' }); + } let userId = await ctx.db.getUserId(userDataResult.data.email); if (!userId) { const newUser = await ctx.db.createUser(userDataResult.data.email); diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 0331e4f1..45ae1556 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -8,3 +8,5 @@ export type VoidResult = VoidSuccess | Failure; export const success = (data: T): Success => ({ success: true, data }); export const failure = (error: E): Failure => ({ success: false, error }); + +export { en as enLocale } from './locales/en/app.js'; diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json index 7fad7f24..7e87852a 100644 --- a/packages/common/tsconfig.json +++ b/packages/common/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "emitDeclarationOnly": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src" }, diff --git a/packages/database/migrations/20240722180545_initial_users_session.mjs b/packages/database/migrations/20240722180545_initial_users_session.mjs index 5a519c93..cff7b085 100644 --- a/packages/database/migrations/20240722180545_initial_users_session.mjs +++ b/packages/database/migrations/20240722180545_initial_users_session.mjs @@ -4,15 +4,17 @@ */ export async function up(knex) { await knex.schema.createTable('users', table => { - table.uuid('id').primary(); //.defaultTo(knex.raw('gen_random_uuid()')); + //table.uuid('id').primary(); //.defaultTo(knex.raw('gen_random_uuid()')); + table.text('id').primary(); table.string('email').notNullable().unique(); table.timestamps(true, true); }); await knex.schema.createTable('sessions', table => { - table.uuid('id').primary(); //.defaultTo(knex.raw('gen_random_uuid()')); + //table.uuid('id').primary(); //.defaultTo(knex.raw('gen_random_uuid()')); + table.text('id').primary(); table - .uuid('user_id') + .text('user_id') .notNullable() .references('id') .inTable('users') diff --git a/packages/database/package.json b/packages/database/package.json index 943c79b9..439e13b8 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -4,11 +4,24 @@ "description": "10x ATJ database", "type": "module", "license": "CC0", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js", + "types": "./dist/types/index.d.ts" + }, + "./testing": { + "import": "./dist/esm/testing.js", + "require": "./dist/cjs/testing.js", + "types": "./dist/types/testing.d.ts" + } + }, + "types": "dist/types/index.d.ts", "scripts": { - "build": "tsc", - "dev": "tsc --watch", + "build": "rollup -c", + "dev": "rollup -c -w", "test": "vitest run --coverage" }, "dependencies": { @@ -20,7 +33,15 @@ "pg": "^8.12.0" }, "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@testcontainers/postgresql": "^10.11.0", "@types/better-sqlite3": "^7.6.11", + "rollup": "^4.20.0", + "rollup-plugin-typescript2": "^0.36.0", + "testcontainers": "^10.11.0", + "typescript": "^5.5.4", "vite-tsconfig-paths": "^4.3.2" } } diff --git a/packages/database/rollup.config.js b/packages/database/rollup.config.js new file mode 100644 index 00000000..53a464ef --- /dev/null +++ b/packages/database/rollup.config.js @@ -0,0 +1,39 @@ +import { builtinModules } from 'module'; + +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import typescript from 'rollup-plugin-typescript2'; + +import pkg from './package.json' assert { type: 'json' }; + +export default { + input: ['src/index.ts', 'src/testing.ts'], + output: [ + { + dir: 'dist/esm', + format: 'esm', + sourcemap: true, + }, + { + dir: 'dist/cjs', + format: 'cjs', + sourcemap: true, + }, + ], + plugins: [ + nodeResolve(), + commonjs(), + json(), + typescript({ + tsconfig: './tsconfig.json', + useTsconfigDeclarationDir: true, + }), + ], + external: (() => { + const dependencies = Object.keys(pkg.dependencies || {}); + const devDependencies = Object.keys(pkg.devDependencies || {}); + const builtins = builtinModules; + return [...new Set([...dependencies, ...devDependencies, ...builtins])]; + })(), +}; diff --git a/packages/database/src/clients/knex.ts b/packages/database/src/clients/knex.ts index 577273fd..8423ead0 100644 --- a/packages/database/src/clients/knex.ts +++ b/packages/database/src/clients/knex.ts @@ -3,12 +3,32 @@ import { fileURLToPath } from 'url'; import knex, { type Knex } from 'knex'; -const getDirname = () => dirname(fileURLToPath(import.meta.url)); -const migrationsDirectory = path.resolve(getDirname(), '../../migrations'); +const migrationsDirectory = path.resolve( + dirname(fileURLToPath(import.meta.url)), + '../../migrations' +); export const createKnex = (config: Knex.Config): Knex => knex(config); -export const getTestKnex = (): Knex => { +export const getPostgresKnex = ( + connectionString: string, + ssl: boolean = false +): Knex => { + return knex({ + client: 'pg', + connection: { + connectionString, + ssl: ssl ? { rejectUnauthorized: false } : false, + }, + useNullAsDefault: true, + migrations: { + directory: migrationsDirectory, + loadExtensions: ['.mjs'], + }, + }); +}; + +export const getInMemoryKnex = (): Knex => { return knex({ client: 'better-sqlite3', connection: { @@ -22,7 +42,7 @@ export const getTestKnex = (): Knex => { }); }; -export const getDevKnex = (path: string): Knex => { +export const getFileSystemKnex = (path: string): Knex => { return knex({ client: 'better-sqlite3', connection: { diff --git a/packages/database/src/clients/kysely/db-helpers.ts b/packages/database/src/clients/kysely/db-helpers.ts new file mode 100644 index 00000000..8fb9869c --- /dev/null +++ b/packages/database/src/clients/kysely/db-helpers.ts @@ -0,0 +1,43 @@ +import { type Engine } from './types.js'; + +/** + * With Postgres, use TIMESTAMP, which Kysely will accept a Date object for. + * With SQLite, use INTEGER, which Kysely will accept a number for. + */ +export const dateValue = (engine: E, date: Date) => + dbValue(engine, date, { + postgres: date => date, + sqlite: date => Math.floor(date.getTime() / 1000), + }); +export type DbDate = ReturnType>; + +/** + * Helper function to map native Typescript values/types to database + * values/types. + */ +const dbValue = < + T, + E extends Engine, + PostgresReturnValue, + SqliteReturnValue, + ReturnValue = E extends 'postgres' + ? PostgresReturnValue + : E extends 'sqlite' + ? SqliteReturnValue + : never, +>( + engine: E, + value: T, + transformers: { + postgres: (val: T) => PostgresReturnValue; + sqlite: (val: T) => SqliteReturnValue; + } +): ReturnValue => { + if (engine === 'postgres') { + return transformers.postgres(value) as unknown as ReturnValue; + } else if (engine === 'sqlite') { + return transformers.sqlite(value) as unknown as ReturnValue; + } else { + throw new Error(`Unsupported engine: ${engine}`); + } +}; diff --git a/packages/database/src/clients/kysely/index.ts b/packages/database/src/clients/kysely/index.ts deleted file mode 100644 index 2b8230b8..00000000 --- a/packages/database/src/clients/kysely/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { Database, DatabaseClient } from './types.js'; -export { createInMemoryDatabase, createSqliteDatabase } from './sqlite3.js'; diff --git a/packages/database/src/clients/kysely/postgres.ts b/packages/database/src/clients/kysely/postgres.ts new file mode 100644 index 00000000..c6bcc597 --- /dev/null +++ b/packages/database/src/clients/kysely/postgres.ts @@ -0,0 +1,18 @@ +import { Kysely, PostgresDialect } from 'kysely'; +import pg from 'pg'; + +import { type Database } from './types.js'; + +export const createPostgresDatabase = ( + connectionString: string, + ssl: boolean +) => { + return new Kysely({ + dialect: new PostgresDialect({ + pool: new pg.Pool({ + connectionString, + ssl: ssl ? { rejectUnauthorized: false } : false, + }), + }), + }); +}; diff --git a/packages/database/src/clients/kysely/sqlite3.ts b/packages/database/src/clients/kysely/sqlite3.ts index 7151b9c7..88e70077 100644 --- a/packages/database/src/clients/kysely/sqlite3.ts +++ b/packages/database/src/clients/kysely/sqlite3.ts @@ -3,7 +3,7 @@ import BetterSqliteDatabase, { type Database as SqliteDatabase, } from 'better-sqlite3'; -import { type Database } from './types'; +import { type Database } from './types.js'; type TestDatabase = { kysely: Kysely; diff --git a/packages/database/src/clients/kysely/types.ts b/packages/database/src/clients/kysely/types.ts index b1ab7d2b..4d810938 100644 --- a/packages/database/src/clients/kysely/types.ts +++ b/packages/database/src/clients/kysely/types.ts @@ -5,6 +5,7 @@ import type { Selectable, Updateable, } from 'kysely'; +import { type DbDate } from './db-helpers.js'; export type Engine = 'sqlite' | 'postgres'; @@ -27,7 +28,7 @@ interface SessionsTable { id: string; user_id: string; session_token: string; - expires_at: T extends 'sqlite' ? number : T extends 'postgres' ? Date : never; + expires_at: DbDate; created_at: Generated; updated_at: Generated; } diff --git a/packages/database/src/clients/test-containers.ts b/packages/database/src/clients/test-containers.ts new file mode 100644 index 00000000..971a9291 --- /dev/null +++ b/packages/database/src/clients/test-containers.ts @@ -0,0 +1,96 @@ +import { + PostgreSqlContainer, + StartedPostgreSqlContainer, +} from '@testcontainers/postgresql'; +import pg from 'pg'; + +export type ConnectionDetails = { + host: string; + port: number; + username: string; + password: string; + database: string; +}; + +declare global { + var postgresTestContainer: Promise; +} + +/** + * Setup PostgreSQL test container. Intended to be exported from a Vitest + * globalSetup module. + * @param param0.provide Vitest provide function + * @returns cleanup function + */ +export const setupPostgresContainer = async ({ + provide, +}: { + provide: (name: string, value: any) => void; +}) => { + // Guard against running multiple PostgreSQL containers using a node.js global + if (global.postgresTestContainer === undefined) { + process.stdout.write('Starting PostgreSQL test container...'); + global.postgresTestContainer = new PostgreSqlContainer().start(); + } else { + process.stdout.write( + 'Using already initialized PostgreSQL test container...' + ); + } + + const container = await global.postgresTestContainer; + process.stdout.write('... Done!\n'); + + const connectionDetails: ConnectionDetails = { + host: container.getHost(), + port: container.getMappedPort(5432), + username: container.getUsername(), + password: container.getPassword(), + database: container.getDatabase(), + }; + + provide('postgresConnectionDetails', connectionDetails); + + return async () => { + process.stdout.write('Stopping PostgreSQL test container...'); + await container.stop(); + process.stdout.write('... Done!\n'); + }; +}; + +export const getConnectionString = (connectionDetails: ConnectionDetails) => { + return `postgresql://${connectionDetails.username}:${connectionDetails.password}@${connectionDetails.host}:${connectionDetails.port}/${connectionDetails.database}`; +}; + +export const createTestDatabase = async ( + connectionDetails: ConnectionDetails +) => { + const databaseName = `testdb_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`; + const connectionString = getConnectionString(connectionDetails); + + const client = new pg.Client({ + connectionString, + }); + await client.connect(); + await client.query(`CREATE DATABASE ${databaseName};`); + await client.end(); + + return { + connectionUri: getConnectionString({ + ...connectionDetails, + database: databaseName, + }), + databaseName, + }; +}; + +export const deleteTestDatabase = async ( + connectionDetails: ConnectionDetails, + databaseName: string +) => { + const client = new pg.Client({ + connectionString: getConnectionString(connectionDetails), + }); + await client.connect(); + await client.query(`DROP DATABASE IF EXISTS ${databaseName};`); + await client.end(); +}; diff --git a/packages/database/src/context/dev.ts b/packages/database/src/context/file-system.ts similarity index 68% rename from packages/database/src/context/dev.ts rename to packages/database/src/context/file-system.ts index d7efcc5d..8d7833dc 100644 --- a/packages/database/src/context/dev.ts +++ b/packages/database/src/context/file-system.ts @@ -5,18 +5,19 @@ import { type Database as SqliteDatabase } from 'better-sqlite3'; import knex, { type Knex } from 'knex'; import { type Kysely } from 'kysely'; -import { - type Database, - createSqliteDatabase, -} from '../clients/kysely/index.js'; +import { type Database } from '../clients/kysely/types.js'; +import { createSqliteDatabase } from '../clients/kysely/sqlite3.js'; import { migrateDatabase } from '../management/migrate-database.js'; import { type DatabaseContext } from './types.js'; -const getDirname = () => dirname(fileURLToPath(import.meta.url)); -const migrationsDirectory = path.resolve(getDirname(), '../../migrations'); +const migrationsDirectory = path.resolve( + dirname(fileURLToPath(import.meta.url)), + '../../migrations' +); -export class DevDatabaseContext implements DatabaseContext { +export class FilesystemDatabaseContext implements DatabaseContext { + public readonly engine = 'sqlite'; knex?: Knex; kysely?: Kysely; sqlite3?: SqliteDatabase; @@ -59,10 +60,19 @@ export class DevDatabaseContext implements DatabaseContext { } return this.kysely; } + + async destroy() { + if (this.kysely) { + await this.kysely.destroy(); + } + if (this.knex) { + await this.knex.destroy(); + } + } } -export const createDevDatabaseContext = async (path: string) => { - const ctx = new DevDatabaseContext(path); +export const createFilesystemDatabaseContext = async (path: string) => { + const ctx = new FilesystemDatabaseContext(path); await migrateDatabase(ctx); return ctx; }; diff --git a/packages/database/src/context/test.ts b/packages/database/src/context/in-memory.ts similarity index 56% rename from packages/database/src/context/test.ts rename to packages/database/src/context/in-memory.ts index 202af06a..62c85f88 100644 --- a/packages/database/src/context/test.ts +++ b/packages/database/src/context/in-memory.ts @@ -2,16 +2,15 @@ import { type Database as SqliteDatabase } from 'better-sqlite3'; import { type Knex } from 'knex'; import { type Kysely } from 'kysely'; -import { getTestKnex } from '../clients/knex.js'; -import { - type Database, - createSqliteDatabase, -} from '../clients/kysely/index.js'; +import { getInMemoryKnex } from '../clients/knex.js'; +import { createSqliteDatabase } from '../clients/kysely/sqlite3.js'; +import { type Database } from '../clients/kysely/types.js'; import { migrateDatabase } from '../management/migrate-database.js'; import { type DatabaseContext } from './types.js'; -export class TestDatabaseContext implements DatabaseContext { +export class InMemoryDatabaseContext implements DatabaseContext { + public readonly engine = 'sqlite'; knex?: Knex; kysely?: Kysely; sqlite3?: SqliteDatabase; @@ -20,7 +19,7 @@ export class TestDatabaseContext implements DatabaseContext { async getKnex() { if (!this.knex) { - this.knex = getTestKnex(); + this.knex = getInMemoryKnex(); } return this.knex; } @@ -40,10 +39,22 @@ export class TestDatabaseContext implements DatabaseContext { } return this.kysely; } + + async destroy() { + if (this.knex && this.sqlite3) { + this.knex.client.releaseConnection(this.sqlite3); + } + if (this.knex) { + await this.knex.destroy(); + } + if (this.kysely) { + await this.kysely.destroy(); + } + } } -export const createTestDatabaseContext = async () => { - const ctx = new TestDatabaseContext(); +export const createInMemoryDatabaseContext = async () => { + const ctx = new InMemoryDatabaseContext(); await migrateDatabase(ctx); return ctx; }; diff --git a/packages/database/src/context/postgres.ts b/packages/database/src/context/postgres.ts new file mode 100644 index 00000000..c6658a4b --- /dev/null +++ b/packages/database/src/context/postgres.ts @@ -0,0 +1,62 @@ +import { type Knex } from 'knex'; +import { type Kysely } from 'kysely'; + +import { getPostgresKnex } from '../clients/knex.js'; +import { type Database } from '../clients/kysely/types.js'; +import { createPostgresDatabase } from '../clients/kysely/postgres.js'; +import { migrateDatabase } from '../management/migrate-database.js'; + +import { type DatabaseContext } from './types.js'; +import { Pool } from 'pg'; + +export class PostgresDatabaseContext implements DatabaseContext { + public readonly engine = 'postgres'; + knex?: Knex; + kysely?: Kysely; + pool?: Pool; + + constructor( + public readonly connectionUri: string, + private ssl: boolean = false + ) {} + + async getKnex() { + if (!this.knex) { + this.knex = getPostgresKnex(this.connectionUri, this.ssl); + } + return this.knex; + } + + async getPostgresPool(): Promise { + const knex = await this.getKnex(); + if (!this.pool) { + this.pool = (await knex.client.acquireConnection()) as Pool; + } + return this.pool; + } + + async getKysely(): Promise> { + if (!this.kysely) { + this.kysely = createPostgresDatabase(this.connectionUri, this.ssl); + } + return this.kysely; + } + + async destroy() { + if (this.kysely) { + await this.kysely.destroy(); + } + if (this.knex) { + await this.knex.destroy(); + } + } +} + +export const createPostgresDatabaseContext = async ( + connectionUri: string, + ssl: boolean +) => { + const ctx = new PostgresDatabaseContext(connectionUri, ssl); + await migrateDatabase(ctx); + return ctx; +}; diff --git a/packages/database/src/context/types.ts b/packages/database/src/context/types.ts index 1c2567b3..0a8e26fb 100644 --- a/packages/database/src/context/types.ts +++ b/packages/database/src/context/types.ts @@ -1,9 +1,11 @@ import { Knex } from 'knex'; import { Kysely } from 'kysely'; -import { Database } from '../clients/kysely'; +import { type Database, type Engine } from '../clients/kysely/types.js'; export interface DatabaseContext { + readonly engine: Engine; getKnex: () => Promise; getKysely: () => Promise>; + destroy: () => Promise; } diff --git a/packages/database/src/gateways/sessions/create-session.test.ts b/packages/database/src/gateways/sessions/create-session.test.ts index 5848107d..fa803a29 100644 --- a/packages/database/src/gateways/sessions/create-session.test.ts +++ b/packages/database/src/gateways/sessions/create-session.test.ts @@ -1,15 +1,14 @@ -import { describe, expect, it } from 'vitest'; +import { expect, it } from 'vitest'; -import { createTestDatabaseContext } from '../../context/test'; -import { createSession } from './create-session'; -import { createUser } from '../users/create-user'; +import { type DbTestContext, describeDatabase } from '../../testing.js'; +import { createUser } from '../users/create-user.js'; +import { createSession } from './create-session.js'; -describe('create session', () => { - it('fails with unknown userId', async () => { - const ctx = await createTestDatabaseContext(); +describeDatabase('create session', () => { + it('fails with unknown userId', async ({ db }) => { expect(() => - createSession(ctx, { - id: '1', + createSession(db.ctx, { + id: '31b72aca-116e-412d-b9b8-467300a53748', expiresAt: new Date(), sessionToken: 'token', userId: 'user-id', @@ -17,14 +16,13 @@ describe('create session', () => { ).rejects.toThrow(); }); - it('works with existing user', async () => { - const ctx = await createTestDatabaseContext(); - const user = await createUser(ctx, 'user@test.gov'); + it('works with existing user', async ({ db }) => { + const user = await createUser(db.ctx, 'user@test.gov'); if (user === null) { expect.fail('User was not created'); } - const sessionId = await createSession(ctx, { - id: '1', + const sessionId = await createSession(db.ctx, { + id: '31b72aca-116e-412d-b9b8-467300a53748', expiresAt: new Date(), sessionToken: 'token', userId: user.id, @@ -33,8 +31,8 @@ describe('create session', () => { expect.fail('Session was not created'); } - const db = await ctx.getKysely(); - const insertedSession = await db + const kysely = await db.ctx.getKysely(); + const insertedSession = await kysely .selectFrom('sessions') .select(['id']) .where('id', '=', sessionId) diff --git a/packages/database/src/gateways/sessions/create-session.ts b/packages/database/src/gateways/sessions/create-session.ts index b4793fc8..a2fd9691 100644 --- a/packages/database/src/gateways/sessions/create-session.ts +++ b/packages/database/src/gateways/sessions/create-session.ts @@ -1,3 +1,4 @@ +import { dateValue } from '../../clients/kysely/db-helpers.js'; import { type DatabaseContext } from '../../context/types.js'; type Session = { @@ -14,7 +15,7 @@ export const createSession = async (ctx: DatabaseContext, session: Session) => { .insertInto('sessions') .values({ id: session.id, - expires_at: Math.floor(session.expiresAt.getTime() / 1000), + expires_at: dateValue(ctx.engine, session.expiresAt), session_token: session.sessionToken, user_id: session.userId, //...session.attributes, diff --git a/packages/database/src/gateways/users/create-user.test.ts b/packages/database/src/gateways/users/create-user.test.ts index c33d4efb..501f60d8 100644 --- a/packages/database/src/gateways/users/create-user.test.ts +++ b/packages/database/src/gateways/users/create-user.test.ts @@ -1,18 +1,18 @@ -import { describe, expect, it } from 'vitest'; +import { expect, it } from 'vitest'; -import { createTestDatabaseContext } from '../../context/test'; -import { createUser } from './create-user'; +import { describeDatabase, type DbTestContext } from '../../testing.js'; -describe('create user', () => { - it('works with unknown email address', async () => { - const ctx = await createTestDatabaseContext(); - const resultUser = await createUser(ctx, 'new-user@email.com'); +import { createUser } from './create-user.js'; + +describeDatabase('create user', () => { + it('works with unknown email address', async ({ db }) => { + const resultUser = await createUser(db.ctx, 'new-user@email.com'); if (resultUser === null) { expect.fail('User was not created'); } - const db = await ctx.getKysely(); - const insertedUser = await db + const kysely = await db.ctx.getKysely(); + const insertedUser = await kysely .selectFrom('users') .select(['email', 'id']) .where('id', '=', resultUser.id) @@ -22,20 +22,19 @@ describe('create user', () => { expect(insertedUser?.id).toEqual(resultUser.id); }); - it('fails with known email address', async () => { - const ctx = await createTestDatabaseContext(); - const existingUserResult = await createUser(ctx, 'new-user@email.com'); + it('fails with known email address', async ({ db }) => { + const existingUserResult = await createUser(db.ctx, 'new-user@email.com'); if (existingUserResult === null) { expect.fail('User was not created'); } - const resultUser = await createUser(ctx, 'new-user@email.com'); + const resultUser = await createUser(db.ctx, 'new-user@email.com'); expect(resultUser).toBeNull(); // Check that there is only one row in the users table - const db = await ctx.getKysely(); - const insertedUser = await db + const kysely = await db.ctx.getKysely(); + const insertedUser = await kysely .selectFrom('users') - .select(db.fn.count('id').as('count')) + .select(kysely.fn.count('id').as('count')) .executeTakeFirst(); expect(Number(insertedUser?.count)).toEqual(1); }); diff --git a/packages/database/src/gateways/users/get-user-id.test.ts b/packages/database/src/gateways/users/get-user-id.test.ts index 640b73ce..8dbaa102 100644 --- a/packages/database/src/gateways/users/get-user-id.test.ts +++ b/packages/database/src/gateways/users/get-user-id.test.ts @@ -1,23 +1,21 @@ import { randomUUID } from 'crypto'; -import { describe, expect, it } from 'vitest'; +import { expect, it } from 'vitest'; -import { createTestDatabaseContext } from '../../context/test'; +import { type DbTestContext, describeDatabase } from '../../testing.js'; -import { getUserId } from './get-user-id'; +import { getUserId } from './get-user-id.js'; -describe('get user id', () => { - it('returns null for non-existent user', async () => { - const ctx = await createTestDatabaseContext(); - const userId = await getUserId(ctx, 'new-user@email.com'); +describeDatabase('get user id', () => { + it('returns null for non-existent user', async ({ db }) => { + const userId = await getUserId(db.ctx, 'new-user@email.com'); expect(userId).to.be.null; }); - it('returns null for non-existent user', async () => { - const ctx = await createTestDatabaseContext(); + it('returns null for non-existent user', async ({ db }) => { const id = randomUUID(); - const db = await ctx.getKysely(); - await db + const kysely = await db.ctx.getKysely(); + await kysely .insertInto('users') .values({ id, @@ -25,7 +23,7 @@ describe('get user id', () => { }) .executeTakeFirst(); - const userId = await getUserId(ctx, 'user@agency.gov'); + const userId = await getUserId(db.ctx, 'user@agency.gov'); expect(userId).not.to.be.null; expect(userId).to.be.equal(id); }); diff --git a/packages/database/src/gateways/users/get-user-id.ts b/packages/database/src/gateways/users/get-user-id.ts index 802b51eb..f4e6506a 100644 --- a/packages/database/src/gateways/users/get-user-id.ts +++ b/packages/database/src/gateways/users/get-user-id.ts @@ -1,4 +1,4 @@ -import { type DatabaseContext } from '../..'; +import { type DatabaseContext } from '../../index.js'; export const getUserId = async (ctx: DatabaseContext, email: string) => { const db = await ctx.getKysely(); diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 60f41e53..f8c936ee 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -1,3 +1,6 @@ +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + import { createService } from '@atj/common'; import { DatabaseContext } from './context/types.js'; @@ -6,14 +9,19 @@ import { createUser } from './gateways/users/create-user.js'; import { getUserId } from './gateways/users/get-user-id.js'; export { - type DevDatabaseContext, - createDevDatabaseContext, -} from './context/dev.js'; -export { createTestDatabaseContext } from './context/test.js'; -export { type Database } from './clients/kysely/index.js'; + type FilesystemDatabaseContext, + createFilesystemDatabaseContext, +} from './context/file-system.js'; +export { createInMemoryDatabaseContext } from './context/in-memory.js'; +export { createPostgresDatabaseContext } from './context/postgres.js'; +export { type Database } from './clients/kysely/types.js'; export { type DatabaseContext } from './context/types.js'; export { migrateDatabase } from './management/migrate-database.js'; +export const getDatabaseTestContainerGlobalSetupPath = () => { + return join(dirname(fileURLToPath(import.meta.url)), '../../vitest.setup.ts'); +}; + export const createDatabaseGateway = (ctx: DatabaseContext) => createService(ctx, { createSession, diff --git a/packages/database/src/management/migrate-database.test.ts b/packages/database/src/management/migrate-database.test.ts index 8382ba00..843b170d 100644 --- a/packages/database/src/management/migrate-database.test.ts +++ b/packages/database/src/management/migrate-database.test.ts @@ -1,21 +1,23 @@ -import { describe, expect, it } from 'vitest'; +import { expect, it } from 'vitest'; -import { createTestDatabaseContext } from '../context/test'; +import { type DbTestContext, describeDatabase } from '../testing.js'; +import { migrateDatabase } from './migrate-database.js'; -import { migrateDatabase } from './migrate-database'; +describeDatabase( + 'Knex migrations', + () => { + it('migrate and rollback successfully', async ({ db }) => { + const rollback = await migrateDatabase(db.ctx); + const knex = await db.ctx.getKnex(); + expect(await knex.schema.hasTable('users')).to.be.true; + expect(await knex.schema.hasTable('sessions')).to.be.true; -describe('Knex migrations', {}, () => { - it('migrate and rollback successfully', async () => { - const ctx = await createTestDatabaseContext(); - const rollback = await migrateDatabase(ctx); - const db = await ctx.getKnex(); - expect(await db.schema.hasTable('users')).to.be.true; - expect(await db.schema.hasTable('sessions')).to.be.true; + await rollback(); + expect(await knex.schema.hasTable('users')).to.be.false; + expect(await knex.schema.hasTable('sessions')).to.be.false; - await rollback(); - expect(await db.schema.hasTable('users')).to.be.false; - expect(await db.schema.hasTable('sessions')).to.be.false; - - await db.destroy(); - }); -}); + await knex.destroy(); + }); + }, + false +); diff --git a/packages/database/src/management/migrate-database.ts b/packages/database/src/management/migrate-database.ts index e1aa341c..0c26637c 100644 --- a/packages/database/src/management/migrate-database.ts +++ b/packages/database/src/management/migrate-database.ts @@ -1,4 +1,4 @@ -import { type DatabaseContext } from '../context/types'; +import { type DatabaseContext } from '../context/types.js'; export const migrateDatabase = async (ctx: DatabaseContext) => { const db = await ctx.getKnex(); diff --git a/packages/database/src/testing.ts b/packages/database/src/testing.ts new file mode 100644 index 00000000..8b025994 --- /dev/null +++ b/packages/database/src/testing.ts @@ -0,0 +1,82 @@ +import { afterEach, beforeEach, describe, inject, SuiteFactory } from 'vitest'; + +import { type Engine } from './clients/kysely/types.js'; +import { + type ConnectionDetails, + createTestDatabase, + deleteTestDatabase, +} from './clients/test-containers.js'; +import { InMemoryDatabaseContext } from './context/in-memory.js'; +import { PostgresDatabaseContext } from './context/postgres.js'; +import { type DatabaseContext } from './context/types.js'; +import { migrateDatabase } from './management/migrate-database.js'; + +export type DbTestContext = { + db: { + engine: Engine; + ctx: DatabaseContext; + }; +}; + +type PostgresDbTestContext = DbTestContext & { + db: DbTestContext['db'] & { + connectionDetails: ConnectionDetails; + databaseName: string; + }; +}; + +export const describeDatabase = ( + name: string, + fn: (...args: Parameters) => ReturnType, + runMigrations: boolean = true +) => { + describe(`PostgreSQL - ${name}`, { timeout: 60000 }, test => { + beforeEach(async context => { + const connectionDetails = inject('postgresConnectionDetails'); + if (!connectionDetails) { + throw new Error('Connection details not found'); + } + + const { connectionUri, databaseName } = + await createTestDatabase(connectionDetails); + const ctx = new PostgresDatabaseContext(connectionUri); + if (runMigrations) { + await migrateDatabase(ctx); + } + context.db = { + engine: 'postgres', + connectionDetails, + ctx, + databaseName, + }; + }); + + afterEach(async ({ db }) => { + await db.ctx.destroy(); + const { connectionDetails, databaseName } = db; + + deleteTestDatabase(connectionDetails, databaseName); + }); + + fn(test); + }); + + describe(`SQLite - ${name}`, test => { + beforeEach(async context => { + const ctx = new InMemoryDatabaseContext(); + if (runMigrations) { + await migrateDatabase(ctx); + } + context.db = { + engine: 'sqlite', + ctx, + }; + }); + + afterEach(async ({ db }) => { + db.ctx.destroy(); + }); + + fn(test); + }); +}; diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json index f1b9b550..855dc453 100644 --- a/packages/database/tsconfig.json +++ b/packages/database/tsconfig.json @@ -1,11 +1,16 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "declaration": true, + "declarationDir": "./dist/types", "emitDeclarationOnly": false, + "module": "ESNext", + "moduleResolution": "Bundler", "outDir": "./dist", "rootDir": "./src" }, - "include": ["src"], + "include": ["vite-env.d.ts", "src"], "exclude": ["./dist"], - "references": [] + "references": [], + "types": ["vite/client", "node", "vitest-env.d.ts"] } diff --git a/packages/database/vite-env.d.ts b/packages/database/vite-env.d.ts new file mode 100644 index 00000000..707c4a2e --- /dev/null +++ b/packages/database/vite-env.d.ts @@ -0,0 +1,10 @@ +import { + type ConnectionDetails, + type globalSetupKey, +} from './src/clients/test-containers'; + +declare module 'vitest' { + export interface ProvidedContext { + postgresConnectionDetails: ConnectionDetails; + } +} diff --git a/packages/database/vitest.config.ts b/packages/database/vitest.config.ts index dd6c589c..3e17cd74 100644 --- a/packages/database/vitest.config.ts +++ b/packages/database/vitest.config.ts @@ -2,4 +2,10 @@ import { defineConfig } from 'vitest/config'; import sharedTestConfig from '../../vitest.shared'; -export default defineConfig(sharedTestConfig); +export default defineConfig({ + ...sharedTestConfig, + test: { + ...sharedTestConfig.test, + globalSetup: ['./vitest.setup.ts'], + }, +}); diff --git a/packages/database/vitest.setup.ts b/packages/database/vitest.setup.ts new file mode 100644 index 00000000..84fccc81 --- /dev/null +++ b/packages/database/vitest.setup.ts @@ -0,0 +1,3 @@ +import { setupPostgresContainer } from './src/clients/test-containers'; + +export default setupPostgresContainer; diff --git a/packages/forms/src/builder/builder.test.ts b/packages/forms/src/builder/builder.test.ts index 23a48df6..46ca6fb8 100644 --- a/packages/forms/src/builder/builder.test.ts +++ b/packages/forms/src/builder/builder.test.ts @@ -1,14 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { BlueprintBuilder } from '.'; -import { createForm, getPattern, Pattern } from '..'; -import { defaultFormConfig } from '../patterns'; -import { type InputPattern } from '../patterns/input'; -import { PageSetPattern } from '../patterns/page-set/config'; -import { PagePattern } from '../patterns/page/config'; -import { FieldsetPattern } from '../patterns/fieldset'; -import { FormSummary } from '../patterns/form-summary'; -import { RadioGroupPattern } from '../patterns/radio-group'; +import { type Pattern, createForm, getPattern } from '../index.js'; +import { defaultFormConfig } from '../patterns/index.js'; +import { type FieldsetPattern } from '../patterns/fieldset/index.js'; +import { type FormSummary } from '../patterns/form-summary.js'; +import { type InputPattern } from '../patterns/input/index.js'; +import { type PagePattern } from '../patterns/page/config.js'; +import { type PageSetPattern } from '../patterns/page-set/config.js'; +import { type RadioGroupPattern } from '../patterns/radio-group.js'; + +import { BlueprintBuilder } from './index.js'; describe('form builder', () => { it('addPattern adds initial pattern of given type', () => { diff --git a/packages/forms/src/builder/index.ts b/packages/forms/src/builder/index.ts index 9132e3dd..0218b03b 100644 --- a/packages/forms/src/builder/index.ts +++ b/packages/forms/src/builder/index.ts @@ -9,20 +9,19 @@ import { type PatternMap, addDocument, addPageToPageSet, + addPatternToFieldset, addPatternToPage, + copyPattern, createDefaultPattern, + createOnePageBlueprint, getPattern, + movePatternBetweenPages, removePatternFromBlueprint, updateFormSummary, updatePatternFromFormData, - createOnePageBlueprint, - addPatternToFieldset, - movePatternBetweenPages, - copyPattern, -} from '..'; -import { type PageSetPattern } from '../patterns/page-set/config'; -import { FieldsetPattern } from '../patterns/fieldset'; -import { PagePattern } from '../patterns/page/config'; +} from '../index.js'; +import { type PageSetPattern } from '../patterns/page-set/config.js'; +import { FieldsetPattern } from '../patterns/fieldset/index.js'; export class BlueprintBuilder { bp: Blueprint; diff --git a/packages/forms/src/components.ts b/packages/forms/src/components.ts index 02321fdc..345a1045 100644 --- a/packages/forms/src/components.ts +++ b/packages/forms/src/components.ts @@ -1,11 +1,11 @@ -import { type FormError, getRootPattern } from '.'; +import { type FormError, getRootPattern } from './index.js'; import { type FormConfig, type Pattern, type PatternId, getPatternConfig, -} from './pattern'; -import { type FormSession, nullSession, sessionIsComplete } from './session'; +} from './pattern.js'; +import { type FormSession, nullSession, sessionIsComplete } from './session.js'; export type TextInputProps = PatternProps<{ type: 'input'; diff --git a/packages/forms/src/config.ts b/packages/forms/src/config.ts index f77b1adc..e9fccf72 100644 --- a/packages/forms/src/config.ts +++ b/packages/forms/src/config.ts @@ -1 +1 @@ -export { defaultFormConfig } from './patterns'; +export { defaultFormConfig } from './patterns/index.js'; diff --git a/packages/forms/src/documents/__tests__/document.test.ts b/packages/forms/src/documents/__tests__/document.test.ts index 59612310..50ff3d44 100644 --- a/packages/forms/src/documents/__tests__/document.test.ts +++ b/packages/forms/src/documents/__tests__/document.test.ts @@ -3,14 +3,14 @@ */ import { describe, expect, it } from 'vitest'; -import { getPattern } from '../..'; -import { BlueprintBuilder } from '../../builder'; -import { defaultFormConfig } from '../../patterns'; -import { type PageSetPattern } from '../../patterns/page-set/config'; -import { type PagePattern } from '../../patterns/page/config'; +import { getPattern } from '../../index.js'; +import { BlueprintBuilder } from '../../builder/index.js'; +import { defaultFormConfig } from '../../patterns/index.js'; +import { type PageSetPattern } from '../../patterns/page-set/config.js'; +import { type PagePattern } from '../../patterns/page/config.js'; -import { addDocument } from '../document'; -import { loadSamplePDF } from './sample-data'; +import { addDocument } from '../document.js'; +import { loadSamplePDF } from './sample-data.js'; describe('addDocument document processing', () => { it('creates expected blueprint', async () => { @@ -26,7 +26,7 @@ describe('addDocument document processing', () => { }, { fetchPdfApiResponse: async () => { - const { mockResponse } = await import('../pdf/mock-response'); + const { mockResponse } = await import('../pdf/mock-response.js'); return mockResponse; }, } diff --git a/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts b/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts index b6e90f8f..37a542a7 100644 --- a/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts +++ b/packages/forms/src/documents/__tests__/doj-pardon-marijuana.test.ts @@ -2,10 +2,10 @@ import { describe, expect, test } from 'vitest'; import { Success } from '@atj/common'; -import { type DocumentFieldMap } from '..'; -import { fillPDF, getDocumentFieldData } from '../pdf'; +import { type DocumentFieldMap } from '../index.js'; +import { fillPDF, getDocumentFieldData } from '../pdf/index.js'; -import { loadSamplePDF } from './sample-data'; +import { loadSamplePDF } from './sample-data.js'; describe('DOJ Pardon Attorney Office - Marijuana pardon application form', () => { /* diff --git a/packages/forms/src/documents/__tests__/dummy.test.ts b/packages/forms/src/documents/__tests__/dummy.test.ts index c0b46c91..bc2ba093 100644 --- a/packages/forms/src/documents/__tests__/dummy.test.ts +++ b/packages/forms/src/documents/__tests__/dummy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { generateDummyPDF } from '..'; +import { generateDummyPDF } from '../index.js'; describe('PDF document generation', () => { it('can produce a dummy PDF with a JSON dump of data', async () => { diff --git a/packages/forms/src/documents/__tests__/extract.test.ts b/packages/forms/src/documents/__tests__/extract.test.ts index 0ef62ed0..ccdaef1f 100644 --- a/packages/forms/src/documents/__tests__/extract.test.ts +++ b/packages/forms/src/documents/__tests__/extract.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { getDocumentFieldData } from '..'; -import { loadSamplePDF } from './sample-data'; +import { getDocumentFieldData } from '../index.js'; +import { loadSamplePDF } from './sample-data.js'; describe('PDF form field extraction', () => { it('extracts data from California UD-105 form', async () => { diff --git a/packages/forms/src/documents/__tests__/fill-pdf.test.ts b/packages/forms/src/documents/__tests__/fill-pdf.test.ts index 3c8da891..8fbc5068 100644 --- a/packages/forms/src/documents/__tests__/fill-pdf.test.ts +++ b/packages/forms/src/documents/__tests__/fill-pdf.test.ts @@ -2,8 +2,8 @@ import { beforeAll, describe, expect, it } from 'vitest'; import { type Failure, type Success } from '@atj/common'; -import { getDocumentFieldData, fillPDF } from '..'; -import { loadSamplePDF } from './sample-data'; +import { getDocumentFieldData, fillPDF } from '../index.js'; +import { loadSamplePDF } from './sample-data.js'; describe('PDF form filler', () => { let pdfBytes: Uint8Array; diff --git a/packages/forms/src/documents/__tests__/sample-data.ts b/packages/forms/src/documents/__tests__/sample-data.ts index e122d9d8..cd21b7bb 100644 --- a/packages/forms/src/documents/__tests__/sample-data.ts +++ b/packages/forms/src/documents/__tests__/sample-data.ts @@ -1,7 +1,11 @@ -import path from 'path'; import { promises as fs } from 'fs'; +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; -const samplesDirectory = path.resolve(__dirname, '../../../sample-documents'); +const samplesDirectory = path.resolve( + dirname(fileURLToPath(import.meta.url)), + '../../../sample-documents' +); export const loadSamplePDF = async (fileName: `${string}.pdf`) => { const samplePdfPath = path.join(samplesDirectory, fileName); diff --git a/packages/forms/src/documents/document.ts b/packages/forms/src/documents/document.ts index 351bbb1a..03fcba03 100644 --- a/packages/forms/src/documents/document.ts +++ b/packages/forms/src/documents/document.ts @@ -5,16 +5,16 @@ import { addPatterns, addPatternMap, updateFormSummary, -} from '..'; -import { InputPattern } from '../patterns/input'; -import { SequencePattern } from '../patterns/sequence'; -import { PDFDocument, getDocumentFieldData } from './pdf'; +} from '../index.js'; +import { InputPattern } from '../patterns/input/index.js'; +import { SequencePattern } from '../patterns/sequence.js'; +import { PDFDocument, getDocumentFieldData } from './pdf/index.js'; import { type FetchPdfApiResponse, processApiResponse, fetchPdfApiResponse, -} from './pdf/parsing-api'; -import { DocumentFieldMap } from './types'; +} from './pdf/parsing-api.js'; +import { DocumentFieldMap } from './types.js'; export type DocumentTemplate = PDFDocument; diff --git a/packages/forms/src/documents/index.ts b/packages/forms/src/documents/index.ts index dfc68596..659458de 100644 --- a/packages/forms/src/documents/index.ts +++ b/packages/forms/src/documents/index.ts @@ -1,6 +1,6 @@ -export * from './document'; -export * from './pdf'; -export * from './types'; +export * from './document.js'; +export * from './pdf/index.js'; +export * from './types.js'; export const SAMPLE_DOCUMENTS = [ { diff --git a/packages/forms/src/documents/pdf/extract.ts b/packages/forms/src/documents/pdf/extract.ts index a0f0ad9d..6b619fe7 100644 --- a/packages/forms/src/documents/pdf/extract.ts +++ b/packages/forms/src/documents/pdf/extract.ts @@ -10,8 +10,8 @@ import { PDFRadioGroup, } from 'pdf-lib'; -import { stringToBase64 } from '../util'; -import type { DocumentFieldValue, DocumentFieldMap } from '../types'; +import { stringToBase64 } from '../util.js'; +import type { DocumentFieldValue, DocumentFieldMap } from '../types.js'; // TODO: copied from pdf-lib acrofield internals, check if it's already exposed outside of acroform somewhere export const getWidgets = async (pdfDoc: PDFDocument): Promise => { diff --git a/packages/forms/src/documents/pdf/generate.ts b/packages/forms/src/documents/pdf/generate.ts index dabfa55a..c563ddbe 100644 --- a/packages/forms/src/documents/pdf/generate.ts +++ b/packages/forms/src/documents/pdf/generate.ts @@ -1,8 +1,8 @@ import { PDFDocument, type PDFForm } from 'pdf-lib'; import { Result } from '@atj/common'; -import { type FormOutput } from '../..'; -import { type PDFFieldType } from '.'; +import { type FormOutput } from '../../index.js'; +import { type PDFFieldType } from './index.js'; export const createFormOutputFieldData = ( output: FormOutput, diff --git a/packages/forms/src/documents/pdf/index.ts b/packages/forms/src/documents/pdf/index.ts index b2b6f5c8..e0dc44e7 100644 --- a/packages/forms/src/documents/pdf/index.ts +++ b/packages/forms/src/documents/pdf/index.ts @@ -1,6 +1,6 @@ -export { getDocumentFieldData } from './extract'; -export * from './generate'; -export { generateDummyPDF } from './generate-dummy'; +export { getDocumentFieldData } from './extract.js'; +export * from './generate.js'; +export { generateDummyPDF } from './generate-dummy.js'; export type PDFDocument = { type: 'pdf'; diff --git a/packages/forms/src/documents/pdf/parsing-api.ts b/packages/forms/src/documents/pdf/parsing-api.ts index 7d886220..fc27520d 100644 --- a/packages/forms/src/documents/pdf/parsing-api.ts +++ b/packages/forms/src/documents/pdf/parsing-api.ts @@ -8,19 +8,19 @@ import { type PatternMap, createPattern, defaultFormConfig, -} from '../..'; +} from '../../index.js'; -import { type FieldsetPattern } from '../../patterns/fieldset'; -import { type InputPattern } from '../../patterns/input'; -import { type ParagraphPattern } from '../../patterns/paragraph'; -import { type CheckboxPattern } from '../../patterns/checkbox'; -import { type RadioGroupPattern } from '../../patterns/radio-group'; -import { type FormSummary } from '../../patterns/form-summary'; +import { type FieldsetPattern } from '../../patterns/fieldset/index.js'; +import { type InputPattern } from '../../patterns/input/index.js'; +import { type ParagraphPattern } from '../../patterns/paragraph.js'; +import { type CheckboxPattern } from '../../patterns/checkbox.js'; +import { type RadioGroupPattern } from '../../patterns/radio-group.js'; +import { type FormSummary } from '../../patterns/form-summary.js'; -import { uint8ArrayToBase64 } from '../util'; -import { type DocumentFieldMap } from '../types'; -import { PagePattern } from '../../patterns/page/config'; -import { PageSetPattern } from '../../patterns/page-set/config'; +import { uint8ArrayToBase64 } from '../util.js'; +import { type DocumentFieldMap } from '../types.js'; +import { PagePattern } from '../../patterns/page/config.js'; +import { PageSetPattern } from '../../patterns/page-set/config.js'; /** API v1 response format * // formSummary json diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index f15d148f..078606a6 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -1,5 +1,5 @@ -import { type SequencePattern } from './patterns/sequence'; -import { type DocumentFieldMap } from './documents'; +import { type SequencePattern } from './patterns/sequence.js'; +import { type DocumentFieldMap } from './documents/index.js'; import { type FormConfig, type Pattern, @@ -8,20 +8,20 @@ import { getPatternMap, removeChildPattern, generatePatternId, -} from './pattern'; -import { type PagePattern } from './patterns/page/config'; -import { type PageSetPattern } from './patterns/page-set/config'; -import { FieldsetPattern } from './patterns/fieldset'; - -export * from './builder'; -export * from './components'; -export * from './config'; -export * from './documents'; -export * from './error'; -export * from './pattern'; -export * from './response'; -export * from './session'; -export * as service from './service'; +} from './pattern.js'; +import { type PagePattern } from './patterns/page/config.js'; +import { type PageSetPattern } from './patterns/page-set/config.js'; +import { FieldsetPattern } from './patterns/fieldset/index.js'; + +export * from './builder/index.js'; +export * from './components.js'; +export * from './config.js'; +export * from './documents/index.js'; +export * from './error.js'; +export * from './pattern.js'; +export * from './response.js'; +export * from './session.js'; +export * as service from './service/index.js'; export type Blueprint = { summary: FormSummary; diff --git a/packages/forms/src/pattern.ts b/packages/forms/src/pattern.ts index 15517ea7..dcd1cb64 100644 --- a/packages/forms/src/pattern.ts +++ b/packages/forms/src/pattern.ts @@ -1,13 +1,12 @@ import * as r from '@atj/common'; import { - addPatternToFieldset, type Blueprint, type FormError, type FormErrors, updatePattern, -} from '.'; +} from './index.js'; -import { type CreatePrompt } from './components'; +import { type CreatePrompt } from './components.js'; export type Pattern = { type: string; diff --git a/packages/forms/src/patterns/address/index.ts b/packages/forms/src/patterns/address/index.ts index afb06958..05d72b39 100644 --- a/packages/forms/src/patterns/address/index.ts +++ b/packages/forms/src/patterns/address/index.ts @@ -1,16 +1,16 @@ import * as z from 'zod'; -import { type Pattern, type PatternConfig } from '../../pattern'; -import { PatternProps } from '../../components'; +import { type Pattern, type PatternConfig } from '../../pattern.js'; +import { PatternProps } from '../../components.js'; import { safeZodParseFormErrors, safeZodParseToFormError, -} from '../../util/zod'; +} from '../../util/zod.js'; import { stateTerritoryOrMilitaryPostAbbreviations, stateTerritoryOrMilitaryPostList, -} from './jurisdictions'; -import { getFormSessionValue } from '../../session'; +} from './jurisdictions.js'; +import { getFormSessionValue } from '../../session.js'; export type AddressPattern = Pattern<{}>; diff --git a/packages/forms/src/patterns/checkbox.ts b/packages/forms/src/patterns/checkbox.ts index 48926154..db6788fb 100644 --- a/packages/forms/src/patterns/checkbox.ts +++ b/packages/forms/src/patterns/checkbox.ts @@ -1,9 +1,16 @@ import * as z from 'zod'; -import { type Pattern, type PatternConfig, validatePattern } from '../pattern'; -import { type CheckboxProps } from '../components'; -import { getFormSessionError, getFormSessionValue } from '../session'; -import { safeZodParseFormErrors, safeZodParseToFormError } from '../util/zod'; +import { + type Pattern, + type PatternConfig, + validatePattern, +} from '../pattern.js'; +import { type CheckboxProps } from '../components.js'; +import { getFormSessionValue } from '../session.js'; +import { + safeZodParseFormErrors, + safeZodParseToFormError, +} from '../util/zod.js'; const configSchema = z.object({ label: z.string().min(1), diff --git a/packages/forms/src/patterns/fieldset/config.ts b/packages/forms/src/patterns/fieldset/config.ts index 00d852dc..cf09c85a 100644 --- a/packages/forms/src/patterns/fieldset/config.ts +++ b/packages/forms/src/patterns/fieldset/config.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { safeZodParseFormErrors } from '../../util/zod'; -import { ParsePatternConfigData } from '../../pattern'; +import { safeZodParseFormErrors } from '../../util/zod.js'; +import { ParsePatternConfigData } from '../../pattern.js'; const configSchema = z.object({ legend: z.string().min(1), diff --git a/packages/forms/src/patterns/fieldset/index.ts b/packages/forms/src/patterns/fieldset/index.ts index a70692b7..c095046c 100644 --- a/packages/forms/src/patterns/fieldset/index.ts +++ b/packages/forms/src/patterns/fieldset/index.ts @@ -2,9 +2,9 @@ import { type Pattern, type PatternConfig, type PatternId, -} from '../../pattern'; -import { parseConfigData } from './config'; -import { createPrompt } from './prompt'; +} from '../../pattern.js'; +import { parseConfigData } from './config.js'; +import { createPrompt } from './prompt.js'; export type FieldsetPattern = Pattern<{ legend?: string; diff --git a/packages/forms/src/patterns/fieldset/prompt.ts b/packages/forms/src/patterns/fieldset/prompt.ts index 7b33d364..977cb56d 100644 --- a/packages/forms/src/patterns/fieldset/prompt.ts +++ b/packages/forms/src/patterns/fieldset/prompt.ts @@ -1,10 +1,10 @@ -import { type FieldsetPattern } from '.'; +import { type FieldsetPattern } from './index.js'; import { type CreatePrompt, type FieldsetProps, createPromptForPattern, getPattern, -} from '../..'; +} from '../../index.js'; export const createPrompt: CreatePrompt = ( config, diff --git a/packages/forms/src/patterns/form-summary.ts b/packages/forms/src/patterns/form-summary.ts index 7f696fe5..0a0a6e34 100644 --- a/packages/forms/src/patterns/form-summary.ts +++ b/packages/forms/src/patterns/form-summary.ts @@ -1,8 +1,8 @@ import * as z from 'zod'; -import { type Pattern, type PatternConfig } from '../pattern'; -import { type FormSummaryProps } from '../components'; -import { safeZodParseFormErrors } from '../util/zod'; +import { type Pattern, type PatternConfig } from '../pattern.js'; +import { type FormSummaryProps } from '../components.js'; +import { safeZodParseFormErrors } from '../util/zod.js'; const configSchema = z.object({ title: z.string().max(128).min(1, 'Title is required'), diff --git a/packages/forms/src/patterns/index.ts b/packages/forms/src/patterns/index.ts index 60accbbc..c8a06ec8 100644 --- a/packages/forms/src/patterns/index.ts +++ b/packages/forms/src/patterns/index.ts @@ -1,15 +1,15 @@ -import { type FormConfig } from '../pattern'; +import { type FormConfig } from '../pattern.js'; -import { addressConfig } from './address'; -import { checkboxConfig } from './checkbox'; -import { fieldsetConfig } from './fieldset'; -import { formSummaryConfig } from './form-summary'; -import { inputConfig } from './input'; -import { pageConfig } from './page'; -import { pageSetConfig } from './page-set'; -import { paragraphConfig } from './paragraph'; -import { radioGroupConfig } from './radio-group'; -import { sequenceConfig } from './sequence'; +import { addressConfig } from './address/index.js'; +import { checkboxConfig } from './checkbox.js'; +import { fieldsetConfig } from './fieldset/index.js'; +import { formSummaryConfig } from './form-summary.js'; +import { inputConfig } from './input/index.js'; +import { pageConfig } from './page/index.js'; +import { pageSetConfig } from './page-set/index.js'; +import { paragraphConfig } from './paragraph.js'; +import { radioGroupConfig } from './radio-group.js'; +import { sequenceConfig } from './sequence.js'; // This configuration reflects what a user of this library would provide for // their usage scenarios. For now, keep here in the form service until we diff --git a/packages/forms/src/patterns/input/config.ts b/packages/forms/src/patterns/input/config.ts index 882613c0..a64c820a 100644 --- a/packages/forms/src/patterns/input/config.ts +++ b/packages/forms/src/patterns/input/config.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { en as message } from '@atj/common/src/locales/en/app'; +import { enLocale as message } from '@atj/common'; -import { ParsePatternConfigData } from '../../pattern'; -import { safeZodParseFormErrors } from '../../util/zod'; +import { ParsePatternConfigData } from '../../pattern.js'; +import { safeZodParseFormErrors } from '../../util/zod.js'; const configSchema = z.object({ label: z.string().min(1, message.patterns.input.fieldLabelRequired), diff --git a/packages/forms/src/patterns/input/index.ts b/packages/forms/src/patterns/input/index.ts index d2d3d702..1ed4f046 100644 --- a/packages/forms/src/patterns/input/index.ts +++ b/packages/forms/src/patterns/input/index.ts @@ -1,10 +1,10 @@ -import { en as message } from '@atj/common/src/locales/en/app'; +import { enLocale as message } from '@atj/common'; -import { Pattern, type PatternConfig } from '../../pattern'; +import { Pattern, type PatternConfig } from '../../pattern.js'; -import { parseConfigData, type InputConfigSchema } from './config'; -import { createPrompt } from './prompt'; -import { type InputPatternOutput, parseUserInput } from './response'; +import { parseConfigData, type InputConfigSchema } from './config.js'; +import { createPrompt } from './prompt.js'; +import { type InputPatternOutput, parseUserInput } from './response.js'; export type InputPattern = Pattern; diff --git a/packages/forms/src/patterns/input/prompt.ts b/packages/forms/src/patterns/input/prompt.ts index 696acc8e..ff92755e 100644 --- a/packages/forms/src/patterns/input/prompt.ts +++ b/packages/forms/src/patterns/input/prompt.ts @@ -1,10 +1,10 @@ -import { type InputPattern, inputConfig } from '.'; +import { type InputPattern, inputConfig } from './index.js'; import { type CreatePrompt, type TextInputProps, getFormSessionValue, validatePattern, -} from '../..'; +} from '../../index.js'; export const createPrompt: CreatePrompt = ( _, diff --git a/packages/forms/src/patterns/input/response.ts b/packages/forms/src/patterns/input/response.ts index 99c96852..30ac0b64 100644 --- a/packages/forms/src/patterns/input/response.ts +++ b/packages/forms/src/patterns/input/response.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { ParseUserInput } from '../../pattern'; -import { safeZodParseToFormError } from '../../util/zod'; +import { ParseUserInput } from '../../pattern.js'; +import { safeZodParseToFormError } from '../../util/zod.js'; -import { type InputPattern } from '.'; +import { type InputPattern } from './index.js'; const createSchema = (data: InputPattern['data']) => { const schema = z.string().max(data.maxLength); diff --git a/packages/forms/src/patterns/page-set/config.ts b/packages/forms/src/patterns/page-set/config.ts index 5e9df08e..0e8a7423 100644 --- a/packages/forms/src/patterns/page-set/config.ts +++ b/packages/forms/src/patterns/page-set/config.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { type Pattern, type ParsePatternConfigData } from '../../pattern'; -import { safeZodParseFormErrors } from '../../util/zod'; +import { type Pattern, type ParsePatternConfigData } from '../../pattern.js'; +import { safeZodParseFormErrors } from '../../util/zod.js'; const configSchema = z.object({ pages: z.array(z.string()), diff --git a/packages/forms/src/patterns/page-set/index.ts b/packages/forms/src/patterns/page-set/index.ts index 7908a42c..556926ac 100644 --- a/packages/forms/src/patterns/page-set/index.ts +++ b/packages/forms/src/patterns/page-set/index.ts @@ -1,7 +1,7 @@ -import { type PatternConfig } from '../../pattern'; +import { type PatternConfig } from '../../pattern.js'; -import { type PageSetPattern, parseConfigData } from './config'; -import { createPrompt } from './prompt'; +import { type PageSetPattern, parseConfigData } from './config.js'; +import { createPrompt } from './prompt.js'; export const pageSetConfig: PatternConfig = { displayName: 'Page set', diff --git a/packages/forms/src/patterns/page-set/prompt.ts b/packages/forms/src/patterns/page-set/prompt.ts index 81fa4d6d..b19ef2d9 100644 --- a/packages/forms/src/patterns/page-set/prompt.ts +++ b/packages/forms/src/patterns/page-set/prompt.ts @@ -6,12 +6,12 @@ import { type PromptAction, createPromptForPattern, getPattern, -} from '../..'; -import { type RouteData } from '../../route-data'; -import { safeZodParseFormErrors } from '../../util/zod'; +} from '../../index.js'; +import { type RouteData } from '../../route-data.js'; +import { safeZodParseFormErrors } from '../../util/zod.js'; -import { type PageSetPattern } from './config'; -import { type PagePattern } from '../page/config'; +import { type PageSetPattern } from './config.js'; +import { type PagePattern } from '../page/config.js'; export const createPrompt: CreatePrompt = ( config, diff --git a/packages/forms/src/patterns/page/config.ts b/packages/forms/src/patterns/page/config.ts index a9e5782c..335737d5 100644 --- a/packages/forms/src/patterns/page/config.ts +++ b/packages/forms/src/patterns/page/config.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { type Pattern, type ParsePatternConfigData } from '../../pattern'; -import { safeZodParseFormErrors } from '../../util/zod'; +import { type Pattern, type ParsePatternConfigData } from '../../pattern.js'; +import { safeZodParseFormErrors } from '../../util/zod.js'; const configSchema = z.object({ title: z.string(), diff --git a/packages/forms/src/patterns/page/index.ts b/packages/forms/src/patterns/page/index.ts index 7bdbad44..36421231 100644 --- a/packages/forms/src/patterns/page/index.ts +++ b/packages/forms/src/patterns/page/index.ts @@ -1,7 +1,7 @@ -import { type PatternConfig } from '../../pattern'; +import { type PatternConfig } from '../../pattern.js'; -import { type PagePattern, parseConfigData } from './config'; -import { createPrompt } from './prompt'; +import { type PagePattern, parseConfigData } from './config.js'; +import { createPrompt } from './prompt.js'; export const pageConfig: PatternConfig = { displayName: 'Page', diff --git a/packages/forms/src/patterns/page/prompt.ts b/packages/forms/src/patterns/page/prompt.ts index 0af8930b..ee0fb996 100644 --- a/packages/forms/src/patterns/page/prompt.ts +++ b/packages/forms/src/patterns/page/prompt.ts @@ -3,9 +3,9 @@ import { type PageProps, createPromptForPattern, getPattern, -} from '../..'; +} from '../../index.js'; -import { type PagePattern } from './config'; +import { type PagePattern } from './config.js'; export const createPrompt: CreatePrompt = ( config, diff --git a/packages/forms/src/patterns/paragraph.ts b/packages/forms/src/patterns/paragraph.ts index 2311e12b..103aa765 100644 --- a/packages/forms/src/patterns/paragraph.ts +++ b/packages/forms/src/patterns/paragraph.ts @@ -1,8 +1,8 @@ import * as z from 'zod'; -import { type Pattern, type PatternConfig } from '../pattern'; -import { type ParagraphProps } from '../components'; -import { safeZodParseFormErrors } from '../util/zod'; +import { type Pattern, type PatternConfig } from '../pattern.js'; +import { type ParagraphProps } from '../components.js'; +import { safeZodParseFormErrors } from '../util/zod.js'; const configSchema = z.object({ text: z.string().min(1), diff --git a/packages/forms/src/patterns/radio-group.ts b/packages/forms/src/patterns/radio-group.ts index c0721199..e66af39f 100644 --- a/packages/forms/src/patterns/radio-group.ts +++ b/packages/forms/src/patterns/radio-group.ts @@ -2,11 +2,15 @@ import * as z from 'zod'; import { Result } from '@atj/common'; -import { type RadioGroupProps } from '../components'; -import { type FormError } from '../error'; -import { type Pattern, type PatternConfig, validatePattern } from '../pattern'; -import { getFormSessionValue } from '../session'; -import { safeZodParseFormErrors } from '../util/zod'; +import { type RadioGroupProps } from '../components.js'; +import { type FormError } from '../error.js'; +import { + type Pattern, + type PatternConfig, + validatePattern, +} from '../pattern.js'; +import { getFormSessionValue } from '../session.js'; +import { safeZodParseFormErrors } from '../util/zod.js'; const configSchema = z.object({ label: z.string().min(1), diff --git a/packages/forms/src/patterns/sequence.ts b/packages/forms/src/patterns/sequence.ts index 6254958f..7222629a 100644 --- a/packages/forms/src/patterns/sequence.ts +++ b/packages/forms/src/patterns/sequence.ts @@ -5,9 +5,9 @@ import { type PatternConfig, type PatternId, getPattern, -} from '../pattern'; -import { type SequenceProps, createPromptForPattern } from '../components'; -import { safeZodParseFormErrors } from '../util/zod'; +} from '../pattern.js'; +import { type SequenceProps, createPromptForPattern } from '../components.js'; +import { safeZodParseFormErrors } from '../util/zod.js'; export type SequencePattern = Pattern<{ patterns: PatternId[]; diff --git a/packages/forms/src/response.ts b/packages/forms/src/response.ts index a5eb3d44..7018654a 100644 --- a/packages/forms/src/response.ts +++ b/packages/forms/src/response.ts @@ -5,9 +5,13 @@ import { getPattern, getPatternConfig, validatePattern, -} from '.'; -import { type PromptAction } from './components'; -import { type FormErrorMap, type FormSession, updateSession } from './session'; +} from './index.js'; +import { type PromptAction } from './components.js'; +import { + type FormErrorMap, + type FormSession, + updateSession, +} from './session.js'; export type PromptResponse = { action: PromptAction['type']; diff --git a/packages/forms/src/route-data.ts b/packages/forms/src/route-data.ts index c3c8e524..346646c2 100644 --- a/packages/forms/src/route-data.ts +++ b/packages/forms/src/route-data.ts @@ -1,5 +1,5 @@ import qs from 'qs'; -import { PatternId } from './pattern'; +import { PatternId } from './pattern.js'; export type RouteData = qs.ParsedQs; diff --git a/packages/forms/src/service/context/browser/form-repo.ts b/packages/forms/src/service/context/browser/form-repo.ts index 9b29c21a..287e94f1 100644 --- a/packages/forms/src/service/context/browser/form-repo.ts +++ b/packages/forms/src/service/context/browser/form-repo.ts @@ -1,5 +1,5 @@ import { Result } from '@atj/common'; -import { type Blueprint } from '../../..'; +import { type Blueprint } from '../../../index.js'; export const getFormFromStorage = ( storage: Storage, diff --git a/packages/forms/src/service/context/browser/index.ts b/packages/forms/src/service/context/browser/index.ts index e7aa2bae..1a90cfe2 100644 --- a/packages/forms/src/service/context/browser/index.ts +++ b/packages/forms/src/service/context/browser/index.ts @@ -1,12 +1,12 @@ -import { type FormConfig, defaultFormConfig } from '../../..'; +import { type FormConfig, defaultFormConfig } from '../../../index.js'; -import { addForm } from '../../operations/add-form'; -import { deleteForm } from '../../operations/delete-form'; -import { getForm } from '../../operations/get-form'; -import { getFormList } from '../../operations/get-form-list'; -import { saveForm } from '../../operations/save-form'; -import { submitForm } from '../../operations/submit-form'; -import type { FormService } from '../../types'; +import { addForm } from '../../operations/add-form.js'; +import { deleteForm } from '../../operations/delete-form.js'; +import { getForm } from '../../operations/get-form.js'; +import { getFormList } from '../../operations/get-form-list.js'; +import { saveForm } from '../../operations/save-form.js'; +import { submitForm } from '../../operations/submit-form.js'; +import type { FormService } from '../../types.js'; type BrowserContext = { storage: Storage; diff --git a/packages/forms/src/service/context/browser/session-repo.ts b/packages/forms/src/service/context/browser/session-repo.ts index 5c505ccc..af441782 100644 --- a/packages/forms/src/service/context/browser/session-repo.ts +++ b/packages/forms/src/service/context/browser/session-repo.ts @@ -1,5 +1,5 @@ import { type Result, type VoidResult } from '@atj/common'; -import { type FormSession } from '../../..'; +import { type FormSession } from '../../../index.js'; export const getSessionFromStorage = ( storage: Storage, diff --git a/packages/forms/src/service/context/test/index.ts b/packages/forms/src/service/context/test/index.ts index b8e02dfe..a3c22b30 100644 --- a/packages/forms/src/service/context/test/index.ts +++ b/packages/forms/src/service/context/test/index.ts @@ -1,7 +1,7 @@ -import { defaultFormConfig } from '../../..'; +import { defaultFormConfig } from '../../../index.js'; -import { createBrowserFormService } from '../browser'; -import { type TestData, createTestStorage } from './storage'; +import { createBrowserFormService } from '../browser/index.js'; +import { type TestData, createTestStorage } from './storage.js'; // In tests, use the browser form service with fakes injected. export const createTestFormService = (testData: TestData = {}) => { diff --git a/packages/forms/src/service/context/test/storage.ts b/packages/forms/src/service/context/test/storage.ts index 40a6238f..c7536a31 100644 --- a/packages/forms/src/service/context/test/storage.ts +++ b/packages/forms/src/service/context/test/storage.ts @@ -1,5 +1,5 @@ -import { type Blueprint } from '../../..'; -import { saveFormToStorage } from '../browser/form-repo'; +import { type Blueprint } from '../../../index.js'; +import { saveFormToStorage } from '../browser/form-repo.js'; export type TestData = Record; diff --git a/packages/forms/src/service/index.ts b/packages/forms/src/service/index.ts index bf751a25..d1770e50 100644 --- a/packages/forms/src/service/index.ts +++ b/packages/forms/src/service/index.ts @@ -1,3 +1,3 @@ -export { createBrowserFormService } from './context/browser'; -export { createTestFormService } from './context/test'; -export { type FormService } from './types'; +export { createBrowserFormService } from './context/browser/index.js'; +export { createTestFormService } from './context/test/index.js'; +export { type FormService } from './types.js'; diff --git a/packages/forms/src/service/operations/add-form.ts b/packages/forms/src/service/operations/add-form.ts index d7d1e756..9a8c8c0d 100644 --- a/packages/forms/src/service/operations/add-form.ts +++ b/packages/forms/src/service/operations/add-form.ts @@ -1,7 +1,7 @@ import { Result } from '@atj/common'; import { Blueprint } from '../../index.js'; -import { addFormToStorage } from '../context/browser/form-repo'; +import { addFormToStorage } from '../context/browser/form-repo.js'; export const addForm = ( ctx: { storage: Storage }, diff --git a/packages/forms/src/service/operations/delete-form.ts b/packages/forms/src/service/operations/delete-form.ts index 2a758e55..b37f1dcf 100644 --- a/packages/forms/src/service/operations/delete-form.ts +++ b/packages/forms/src/service/operations/delete-form.ts @@ -3,7 +3,7 @@ import { VoidResult } from '@atj/common'; import { deleteFormFromStorage, getFormFromStorage, -} from '../context/browser/form-repo'; +} from '../context/browser/form-repo.js'; export const deleteForm = ( ctx: { storage: Storage }, diff --git a/packages/forms/src/service/operations/get-form-list.ts b/packages/forms/src/service/operations/get-form-list.ts index 198fb48b..1e5784b6 100644 --- a/packages/forms/src/service/operations/get-form-list.ts +++ b/packages/forms/src/service/operations/get-form-list.ts @@ -1,6 +1,6 @@ import { Result } from '@atj/common'; -import { getFormSummaryListFromStorage } from '../context/browser/form-repo'; +import { getFormSummaryListFromStorage } from '../context/browser/form-repo.js'; export type FormListItem = { id: string; diff --git a/packages/forms/src/service/operations/get-form.ts b/packages/forms/src/service/operations/get-form.ts index b1f32e5e..256d64d2 100644 --- a/packages/forms/src/service/operations/get-form.ts +++ b/packages/forms/src/service/operations/get-form.ts @@ -1,7 +1,7 @@ import { Result } from '@atj/common'; import { type Blueprint } from '../../index.js'; -import { getFormFromStorage } from '../context/browser/form-repo'; +import { getFormFromStorage } from '../context/browser/form-repo.js'; export const getForm = ( ctx: { storage: Storage }, diff --git a/packages/forms/src/service/operations/save-form.ts b/packages/forms/src/service/operations/save-form.ts index b3a1b170..b71a421a 100644 --- a/packages/forms/src/service/operations/save-form.ts +++ b/packages/forms/src/service/operations/save-form.ts @@ -1,7 +1,7 @@ import { Result } from '@atj/common'; import { Blueprint } from '../../index.js'; -import { saveFormToStorage } from '../context/browser/form-repo'; +import { saveFormToStorage } from '../context/browser/form-repo.js'; export const saveForm = ( ctx: { storage: Storage }, diff --git a/packages/forms/src/service/operations/submit-form.test.ts b/packages/forms/src/service/operations/submit-form.test.ts index 4a389636..332e31c8 100644 --- a/packages/forms/src/service/operations/submit-form.test.ts +++ b/packages/forms/src/service/operations/submit-form.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { createForm, createFormSession } from '../../index.js'; -import { createTestFormService } from '../context/test'; +import { createTestFormService } from '../context/test/index.js'; describe('submitForm', () => { it('succeeds with empty form', async () => { diff --git a/packages/forms/src/service/types.ts b/packages/forms/src/service/types.ts index 965873c0..489a0589 100644 --- a/packages/forms/src/service/types.ts +++ b/packages/forms/src/service/types.ts @@ -1,7 +1,7 @@ import { type Result, type VoidResult } from '@atj/common'; -import { type FormListItem } from './operations/get-form-list'; -import { type Blueprint, type FormSession } from '..'; +import { type FormListItem } from './operations/get-form-list.js'; +import { type Blueprint, type FormSession } from '../index.js'; export type FormService = { addForm: (form: Blueprint) => Result<{ timestamp: Date; id: string }>; diff --git a/packages/forms/src/session.ts b/packages/forms/src/session.ts index 2e86ff18..84083c53 100644 --- a/packages/forms/src/session.ts +++ b/packages/forms/src/session.ts @@ -5,14 +5,14 @@ import { type Pattern, getPatternConfig, validatePattern, -} from '.'; -import { SequencePattern } from './patterns/sequence'; +} from './index.js'; +import { SequencePattern } from './patterns/sequence.js'; import { type PatternId, type PatternValue, type PatternValueMap, -} from './pattern'; -import { type RouteData, getRouteDataFromQueryString } from './route-data'; +} from './pattern.js'; +import { type RouteData, getRouteDataFromQueryString } from './route-data.js'; export type FormErrorMap = Record; diff --git a/packages/forms/src/util/transform.ts b/packages/forms/src/util/transform.ts index d7633b62..d307f8b4 100644 --- a/packages/forms/src/util/transform.ts +++ b/packages/forms/src/util/transform.ts @@ -1,5 +1,5 @@ -import { Field } from '../field'; -import { capitalizeFirstLetter } from '../util/string-format'; +import { Field } from '../field.js'; +import { capitalizeFirstLetter } from '../util/string-format.js'; export type FieldTransformer = (field: Field) => Field; diff --git a/packages/forms/src/util/zod.ts b/packages/forms/src/util/zod.ts index 41339bfe..097b0437 100644 --- a/packages/forms/src/util/zod.ts +++ b/packages/forms/src/util/zod.ts @@ -2,7 +2,7 @@ import * as z from 'zod'; import * as r from '@atj/common'; -import { type FormError, type FormErrors, type Pattern } from '..'; +import { type FormError, type FormErrors, type Pattern } from '../index.js'; export const safeZodParse = ( schema: z.Schema, diff --git a/packages/forms/tsconfig.json b/packages/forms/tsconfig.json index 73454b0b..a44877a3 100644 --- a/packages/forms/tsconfig.json +++ b/packages/forms/tsconfig.json @@ -2,6 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "emitDeclarationOnly": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src" }, diff --git a/packages/server/src/context.ts b/packages/server/src/context.ts index 6501b4dd..5cbfd987 100644 --- a/packages/server/src/context.ts +++ b/packages/server/src/context.ts @@ -7,7 +7,7 @@ import type { AuthContext, LoginGovOptions } from '@atj/auth'; import { type DatabaseGateway } from '@atj/database'; import { type FormConfig, defaultFormConfig, service } from '@atj/forms'; -import { type GithubRepository } from './lib/github'; +import { type GithubRepository } from './lib/github.js'; export type AppContext = { auth: AuthContext; @@ -23,6 +23,7 @@ export type ServerOptions = { title: string; db: DatabaseGateway; loginGovOptions: LoginGovOptions; + isUserAuthorized: (email: string) => Promise; }; export const getAstroAppContext = async (Astro: any): Promise => { @@ -42,6 +43,7 @@ const createAstroAppContext = async ( Astro, db: serverOptions.db, loginGovOptions: serverOptions.loginGovOptions, + isUserAuthorized: serverOptions.isUserAuthorized, }), baseUrl: env.BASE_URL, formConfig: defaultFormConfig, @@ -64,6 +66,9 @@ const getDefaultServerOptions = async (): Promise => { //clientSecret: import.meta.env.SECRET_LOGIN_GOV_PRIVATE_KEY, redirectURI: 'http://localhost:4322/signin/callback', }, + isUserAuthorized: async (email: string) => { + return true; + }, }; }; @@ -74,10 +79,11 @@ const getServerOptions = async (Astro: AstroGlobal | APIContext) => { const getDirname = () => dirname(fileURLToPath(import.meta.url)); const createDefaultDatabaseGateway = async () => { - const { createDatabaseGateway, createDevDatabaseContext } = await import( - '@atj/database' + const { createDatabaseGateway, createFilesystemDatabaseContext } = + await import('@atj/database'); + const ctx = await createFilesystemDatabaseContext( + join(getDirname(), '../main.db') ); - const ctx = await createDevDatabaseContext(join(getDirname(), '../main.db')); const gateway = createDatabaseGateway(ctx); return Promise.resolve(gateway); }; @@ -94,13 +100,15 @@ const createDefaultAuthContext = async ({ Astro, db, loginGovOptions, + isUserAuthorized, }: { Astro: AstroGlobal | APIContext; db: DatabaseGateway; loginGovOptions: LoginGovOptions; + isUserAuthorized: (email: string) => Promise; }) => { - const { LoginGov, DevAuthContext } = await import('@atj/auth'); - return new DevAuthContext( + const { LoginGov, BaseAuthContext } = await import('@atj/auth'); + return new BaseAuthContext( db, new LoginGov({ ...loginGovOptions, @@ -115,7 +123,8 @@ const createDefaultAuthContext = async ({ function setUserSession({ session, user }) { Astro.locals.session = session; Astro.locals.user = user; - } + }, + isUserAuthorized ); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4f26d07..8530ea04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,6 +212,9 @@ importers: '@atj/database': specifier: workspace:* version: link:../database + '@lucia-auth/adapter-postgresql': + specifier: ^3.1.2 + version: 3.1.2(lucia@3.2.0) '@lucia-auth/adapter-sqlite': specifier: ^3.0.2 version: 3.0.2(better-sqlite3@11.1.2)(lucia@3.2.0) @@ -258,9 +261,33 @@ importers: specifier: ^8.12.0 version: 8.12.0 devDependencies: + '@rollup/plugin-commonjs': + specifier: ^26.0.1 + version: 26.0.1(rollup@4.20.0) + '@rollup/plugin-json': + specifier: ^6.1.0 + version: 6.1.0(rollup@4.20.0) + '@rollup/plugin-node-resolve': + specifier: ^15.2.3 + version: 15.2.3(rollup@4.20.0) + '@testcontainers/postgresql': + specifier: ^10.11.0 + version: 10.11.0 '@types/better-sqlite3': specifier: ^7.6.11 version: 7.6.11 + rollup: + specifier: ^4.20.0 + version: 4.20.0 + rollup-plugin-typescript2: + specifier: ^0.36.0 + version: 0.36.0(rollup@4.20.0)(typescript@5.5.4) + testcontainers: + specifier: ^10.11.0 + version: 10.11.0 + typescript: + specifier: ^5.5.4 + version: 5.5.4 vite-tsconfig-paths: specifier: ^4.3.2 version: 4.3.2(typescript@5.5.4) @@ -2428,6 +2455,10 @@ packages: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + /@balena/dockerignore@1.0.2: + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + dev: true + /@base2/pretty-print-object@1.0.1: resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==} dev: true @@ -3343,6 +3374,11 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: true + /@gulp-sourcemaps/identity-map@2.0.1: resolution: {integrity: sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==} engines: {node: '>= 0.10'} @@ -4072,6 +4108,24 @@ packages: ajv: 8.17.1 dev: false + /@lucia-auth/adapter-postgresql@3.1.2(lucia@3.2.0): + resolution: {integrity: sha512-XgScy312JsaiyJZ0OaUHakk01hFBldF1m8abX4Ctk2Dkt7lEVS/u0xZ2Sf6Hlji1wZtEM0uiIWdeMUu79XcXZA==} + peerDependencies: + '@neondatabase/serverless': 0.7 - 0.9 + lucia: 3.x + pg: ^8.8.0 + postgres: ^3.3.0 + peerDependenciesMeta: + '@neondatabase/serverless': + optional: true + pg: + optional: true + postgres: + optional: true + dependencies: + lucia: 3.2.0 + dev: false + /@lucia-auth/adapter-sqlite@3.0.2(better-sqlite3@11.1.2)(lucia@3.2.0): resolution: {integrity: sha512-UlXpF+2UoFEdm1AsriJii5BOARwqko6SX29rQ8T8Za7rnjj9KLXLaRVQUgBhGmggAyvzCtguJ2+XOZDsfWm6Sw==} peerDependencies: @@ -4838,7 +4892,64 @@ packages: engines: {node: '>=14.0.0'} dev: false - /@rollup/pluginutils@5.1.0: + /@rollup/plugin-commonjs@26.0.1(rollup@4.20.0): + resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 10.4.5 + is-reference: 1.2.1 + magic-string: 0.30.11 + rollup: 4.20.0 + dev: true + + /@rollup/plugin-json@6.1.0(rollup@4.20.0): + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) + rollup: 4.20.0 + dev: true + + /@rollup/plugin-node-resolve@15.2.3(rollup@4.20.0): + resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.8 + rollup: 4.20.0 + dev: true + + /@rollup/pluginutils@4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@rollup/pluginutils@5.1.0(rollup@4.20.0): resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} peerDependencies: @@ -4850,6 +4961,7 @@ packages: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 + rollup: 4.20.0 dev: true /@rollup/rollup-android-arm-eabi@4.20.0: @@ -5889,7 +6001,7 @@ packages: vite: ^4.0.0 || ^5.0.0 dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.5.4)(vite@5.4.0) - '@rollup/pluginutils': 5.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) '@storybook/builder-vite': 8.2.8(storybook@8.2.8)(typescript@5.5.4)(vite@5.4.0) '@storybook/react': 8.2.8(react-dom@18.3.1)(react@18.3.1)(storybook@8.2.8)(typescript@5.5.4) find-up: 5.0.0 @@ -6198,6 +6310,14 @@ packages: '@swc/counter': 0.1.3 dev: true + /@testcontainers/postgresql@10.11.0: + resolution: {integrity: sha512-TJC6kyb2lmkSF2XWvsjDVn61YRin8e1mE2IiLRkeR3mKWHm/LDwyRX14RTnRuNK7auSCCr35Ft/fKv/R6O5Taw==} + dependencies: + testcontainers: 10.11.0 + transitivePeerDependencies: + - supports-color + dev: true + /@testing-library/dom@10.1.0: resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} engines: {node: '>=18'} @@ -6405,6 +6525,21 @@ packages: resolution: {integrity: sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA==} dev: true + /@types/docker-modem@3.0.6: + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + dependencies: + '@types/node': 20.14.14 + '@types/ssh2': 1.15.1 + dev: true + + /@types/dockerode@3.3.31: + resolution: {integrity: sha512-42R9eoVqJDSvVspV89g7RwRqfNExgievLNWoHkg7NoWIqAmavIbgQBb4oc0qRtHkxE+I3Xxvqv7qVXFABKPBTg==} + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 20.14.14 + '@types/ssh2': 1.15.1 + dev: true + /@types/doctrine@0.0.9: resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} dev: true @@ -6598,6 +6733,10 @@ packages: '@types/prop-types': 15.7.12 csstype: 3.1.3 + /@types/resolve@1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + dev: true + /@types/resolve@1.20.6: resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} dev: true @@ -6618,6 +6757,25 @@ packages: '@types/node': 22.1.0 '@types/send': 0.17.4 + /@types/ssh2-streams@0.1.12: + resolution: {integrity: sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==} + dependencies: + '@types/node': 20.14.14 + dev: true + + /@types/ssh2@0.5.52: + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + dependencies: + '@types/node': 20.14.14 + '@types/ssh2-streams': 0.1.12 + dev: true + + /@types/ssh2@1.15.1: + resolution: {integrity: sha512-ZIbEqKAsi5gj35y4P4vkJYly642wIbY6PqoN0xiyQGshKUGXR9WQjF/iF9mXBQ8uBKy3ezfsCkcoHKhd0BzuDA==} + dependencies: + '@types/node': 18.19.43 + dev: true + /@types/ssri@7.1.5: resolution: {integrity: sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==} dependencies: @@ -7129,6 +7287,13 @@ packages: isexe: 2.0.0 dev: false + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -7446,6 +7611,19 @@ packages: readable-stream: 3.6.2 dev: false + /archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + dependencies: + glob: 10.4.5 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + dev: true + /archiver@5.3.2: resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} engines: {node: '>= 10'} @@ -7472,6 +7650,19 @@ packages: zip-stream: 5.0.2 dev: false + /archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + dependencies: + archiver-utils: 5.0.2 + async: 3.2.5 + buffer-crc32: 1.0.0 + readable-stream: 4.5.2 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + dev: true + /archy@1.0.0: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} @@ -7669,6 +7860,12 @@ packages: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} dev: true + /asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: true + /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} dev: true @@ -7798,6 +7995,10 @@ packages: resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} dev: true + /async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + dev: true + /async-settle@1.0.0: resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==} engines: {node: '>= 0.10'} @@ -8030,6 +8231,38 @@ packages: requiresBuild: true optional: true + /bare-fs@2.3.1: + resolution: {integrity: sha512-W/Hfxc/6VehXlsgFtbB5B4xFcsCl+pAh30cYhoFyXErf6oGrwjh8SwiPAdHgpmWonKuYpZgGywN0SXt7dgsADA==} + requiresBuild: true + dependencies: + bare-events: 2.4.2 + bare-path: 2.1.3 + bare-stream: 2.1.3 + dev: true + optional: true + + /bare-os@2.4.0: + resolution: {integrity: sha512-v8DTT08AS/G0F9xrhyLtepoo9EJBJ85FRSMbu1pQUlAf6A8T0tEEQGMVObWeqpjhSPXsE0VGlluFBJu2fdoTNg==} + requiresBuild: true + dev: true + optional: true + + /bare-path@2.1.3: + resolution: {integrity: sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==} + requiresBuild: true + dependencies: + bare-os: 2.4.0 + dev: true + optional: true + + /bare-stream@2.1.3: + resolution: {integrity: sha512-tiDAH9H/kP+tvNO5sczyn9ZAA7utrSMobyDchsnyyXBuUe2FSQWbxhtuHB8jwpHYYevVo2UJpcmvvjrbHboUUQ==} + requiresBuild: true + dependencies: + streamx: 2.18.0 + dev: true + optional: true + /base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} dev: false @@ -8057,6 +8290,12 @@ packages: safe-buffer: 5.1.2 dev: true + /bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: true + /better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -8244,6 +8483,11 @@ packages: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} dev: false + /buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + dev: true + /buffer-equal@1.0.1: resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} engines: {node: '>=0.4'} @@ -8266,6 +8510,18 @@ packages: ieee754: 1.2.1 dev: true + /buildcheck@0.0.6: + resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dev: true + optional: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + /bundle-require@5.0.0(esbuild@0.23.0): resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8276,6 +8532,11 @@ packages: load-tsconfig: 0.2.5 dev: true + /byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + dev: true + /bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -8577,7 +8838,6 @@ packages: /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} requiresBuild: true - dev: false /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} @@ -8932,6 +9192,17 @@ packages: readable-stream: 3.6.2 dev: false + /compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.5.2 + dev: true + /computeds@0.0.1: resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} dev: true @@ -9051,11 +9322,20 @@ packages: engines: {node: '>= 0.4.0'} dev: true + /cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dependencies: + buildcheck: 0.0.6 + nan: 2.20.0 + dev: true + optional: true + /crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} hasBin: true - dev: false /crc32-stream@4.0.3: resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} @@ -9073,6 +9353,14 @@ packages: readable-stream: 3.6.2 dev: false + /crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + dependencies: + crc-32: 1.2.2 + readable-stream: 4.5.2 + dev: true + /create-jest@29.7.0(@types/node@20.14.14)(ts-node@10.9.2): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9609,6 +9897,36 @@ packages: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dev: false + /docker-compose@0.24.8: + resolution: {integrity: sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==} + engines: {node: '>= 6.0.0'} + dependencies: + yaml: 2.5.0 + dev: true + + /docker-modem@3.0.8: + resolution: {integrity: sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==} + engines: {node: '>= 8.0'} + dependencies: + debug: 4.3.6 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.15.0 + transitivePeerDependencies: + - supports-color + dev: true + + /dockerode@3.3.5: + resolution: {integrity: sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==} + engines: {node: '>= 8.0'} + dependencies: + '@balena/dockerignore': 1.0.2 + docker-modem: 3.0.8 + tar-fs: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -9688,7 +10006,7 @@ packages: dependencies: semver: 7.6.3 shelljs: 0.8.5 - typescript: 5.6.0-dev.20240810 + typescript: 5.6.0-dev.20240819 dev: false /dset@3.1.3: @@ -10283,6 +10601,11 @@ packages: es5-ext: 0.10.64 dev: true + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: true @@ -10294,7 +10617,6 @@ packages: /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - dev: false /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} @@ -10884,7 +11206,6 @@ packages: /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} requiresBuild: true - dev: false /fs-exists-sync@0.1.0: resolution: {integrity: sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==} @@ -10898,7 +11219,6 @@ packages: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: false /fs-extra@11.2.0: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} @@ -11036,6 +11356,11 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + /get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + dev: true + /get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} dependencies: @@ -12190,6 +12515,13 @@ packages: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: true + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -12333,6 +12665,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + /is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + dev: true + /is-negated-glob@1.0.0: resolution: {integrity: sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==} engines: {node: '>=0.10.0'} @@ -12404,6 +12740,12 @@ packages: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: true + /is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.5 + dev: true + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -14597,7 +14939,6 @@ packages: /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} requiresBuild: true - dev: false /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} @@ -15956,6 +16297,21 @@ packages: react-is: 16.13.1 dev: true + /proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + dev: true + + /properties-reader@2.3.0: + resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} + engines: {node: '>=14'} + dependencies: + mkdirp: 1.0.4 + dev: true + /property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} dev: false @@ -15992,7 +16348,6 @@ packages: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - dev: false /pumpify@1.5.1: resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} @@ -16278,11 +16633,21 @@ packages: string_decoder: 1.3.0 util-deprecate: 1.0.2 + /readable-stream@4.5.2: + resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + dev: true + /readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} dependencies: minimatch: 5.1.6 - dev: false /readdirp@2.2.1: resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} @@ -16742,6 +17107,11 @@ packages: unified: 11.0.5 dev: false + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -16772,6 +17142,21 @@ packages: glob: 7.2.3 dev: true + /rollup-plugin-typescript2@0.36.0(rollup@4.20.0)(typescript@5.5.4): + resolution: {integrity: sha512-NB2CSQDxSe9+Oe2ahZbf+B4bh7pHwjV5L+RSYpCu7Q5ROuN94F9b6ioWwKfz3ueL3KTtmX4o2MUH2cgHDIEUsw==} + peerDependencies: + rollup: '>=1.26.3' + typescript: '>=2.4.0' + dependencies: + '@rollup/pluginutils': 4.2.1 + find-cache-dir: 3.3.2 + fs-extra: 10.1.0 + rollup: 4.20.0 + semver: 7.6.3 + tslib: 2.6.3 + typescript: 5.5.4 + dev: true + /rollup@4.20.0: resolution: {integrity: sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -17454,6 +17839,10 @@ packages: engines: {node: '>=8'} dev: false + /split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + dev: true + /split-string@3.1.0: resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} engines: {node: '>=0.10.0'} @@ -17480,6 +17869,25 @@ packages: engines: {node: '>= 12.13.0'} dev: false + /ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.15.0 + dev: true + + /ssh2@1.15.0: + resolution: {integrity: sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==} + engines: {node: '>=10.16.0'} + requiresBuild: true + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.20.0 + dev: true + /stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: true @@ -17916,6 +18324,15 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true + /tar-fs@2.0.1: + resolution: {integrity: sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: true + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} requiresBuild: true @@ -17926,6 +18343,16 @@ packages: tar-stream: 2.2.0 dev: false + /tar-fs@3.0.6: + resolution: {integrity: sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==} + dependencies: + pump: 3.0.0 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 2.3.1 + bare-path: 2.1.3 + dev: true + /tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -17936,7 +18363,6 @@ packages: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 - dev: false /tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -17944,7 +18370,6 @@ packages: b4a: 1.6.6 fast-fifo: 1.3.2 streamx: 2.18.0 - dev: false /tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} @@ -18016,6 +18441,28 @@ packages: minimatch: 9.0.5 dev: true + /testcontainers@10.11.0: + resolution: {integrity: sha512-TYgpR+MjZSuX7kSUxTa0f/CsN6eErbMFrAFumW08IvOnU8b+EoRzpzEu7mF0d29M1ItnHfHPUP44HYiE4yP3Zg==} + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 3.3.31 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.3.6 + docker-compose: 0.24.8 + dockerode: 3.3.5 + get-port: 5.1.1 + proper-lockfile: 4.1.2 + properties-reader: 2.3.0 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.0.6 + tmp: 0.2.3 + undici: 5.28.4 + transitivePeerDependencies: + - supports-color + dev: true + /text-decoder@1.1.1: resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} dependencies: @@ -18122,6 +18569,11 @@ packages: os-tmpdir: 1.0.2 dev: false + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + dev: true + /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} dev: true @@ -18467,6 +18919,10 @@ packages: turbo-windows-arm64: 1.13.4 dev: true + /tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -18605,8 +19061,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - /typescript@5.6.0-dev.20240810: - resolution: {integrity: sha512-X1G99j+LKr2crVMNe9WpoKgzRSK3yv32I/HJU+9Fa8WPeHGmHooAkdZSUaY1U9rYRs3m2Ueg4S+GLwy8RpQZKg==} + /typescript@5.6.0-dev.20240819: + resolution: {integrity: sha512-uNOMaNm8jBELjhuXZG5tSS6Pa6O/Wf89hawKkWaZcZvbkgkY2ykvNTNrkCP7QCQTSS3jwEVvd+pRhxJPxUeG4g==} engines: {node: '>=14.17'} hasBin: true dev: false @@ -18674,6 +19130,13 @@ packages: /undici-types@6.13.0: resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: true + /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -19123,7 +19586,7 @@ packages: optional: true dependencies: '@microsoft/api-extractor': 7.47.4(@types/node@20.14.14) - '@rollup/pluginutils': 5.1.0 + '@rollup/pluginutils': 5.1.0(rollup@4.20.0) '@volar/typescript': 2.3.4 '@vue/language-core': 2.0.29(typescript@5.5.4) compare-versions: 6.1.1 @@ -19828,6 +20291,12 @@ packages: engines: {node: '>= 6'} dev: true + /yaml@2.5.0: + resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} + engines: {node: '>= 14'} + hasBin: true + dev: true + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -19956,6 +20425,15 @@ packages: readable-stream: 3.6.2 dev: false + /zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.5.2 + dev: true + /zod-to-json-schema@3.23.2(zod@3.23.8): resolution: {integrity: sha512-uSt90Gzc/tUfyNqxnjlfBs8W6WSGpNBv0rVsNxP/BVSMHMKGdthPYff4xtCHYloJGM0CFxFsb3NbC0eqPhfImw==} peerDependencies: