Compare commits
7 Commits
61fdd3329f
...
94b6de5970
| Author | SHA1 | Date | |
|---|---|---|---|
|
94b6de5970
|
|||
|
6fa4da1abb
|
|||
|
92f7e1df09
|
|||
|
b2fba6e150
|
|||
|
ebc85af62c
|
|||
|
6b8dd27898
|
|||
|
848d9e9af1
|
8
.ast-grep/rules/no-countall-number.yml
Normal file
8
.ast-grep/rules/no-countall-number.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
id: no-countall-number
|
||||||
|
language: typescript
|
||||||
|
severity: error
|
||||||
|
message: "Don't use countAll<number>() - use countAll() instead. PostgreSQL COUNT returns bigint (string), so the type annotation is misleading."
|
||||||
|
note: "Use Number() to convert the result if you need a number type."
|
||||||
|
rule:
|
||||||
|
pattern: countAll<number>()
|
||||||
|
fix: countAll()
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
# Claude Code Notes
|
# Claude Code Notes
|
||||||
|
|
||||||
|
## Database Scripts
|
||||||
|
|
||||||
|
Use the wrapper scripts instead of running dbmate directly:
|
||||||
|
- `./scripts/db-dump` - Dump schema without random `\restrict` tokens
|
||||||
|
- `./scripts/db-migrate` - Run migrations and dump clean schema
|
||||||
|
|
||||||
|
PostgreSQL 17.6+ adds random `\restrict`/`\unrestrict` lines to pg_dump output (CVE-2025-8714 fix), causing schema.sql to show as changed on every dump. These scripts strip those lines.
|
||||||
|
|
||||||
## Development Server
|
## Development Server
|
||||||
|
|
||||||
Before starting the dev server, check if it's already running:
|
Before starting the dev server, check if it's already running:
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ bun run dev
|
|||||||
| `bun run lint:fix` | Fix linting issues |
|
| `bun run lint:fix` | Fix linting issues |
|
||||||
| `bun run test` | Run tests |
|
| `bun run test` | Run tests |
|
||||||
| `bun run db:codegen` | Generate database types |
|
| `bun run db:codegen` | Generate database types |
|
||||||
|
| `./scripts/db-dump` | Dump database schema (strips `\restrict` lines) |
|
||||||
|
| `./scripts/db-migrate` | Run migrations (strips `\restrict` lines) |
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint . --cache",
|
"lint": "eslint . --cache",
|
||||||
"clean": "rm -rf dist .eslintcache",
|
"clean": "rm -rf dist .eslintcache",
|
||||||
"test:e2e": "bun test src/__tests__/e2e --no-parallel --coverage",
|
"test": "bun test src/ --no-parallel"
|
||||||
"test:unit": "bun test src/__tests__/unit",
|
|
||||||
"test": "bun test --coverage src/utils"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-durationformat": "^0.9.2",
|
"@formatjs/intl-durationformat": "^0.9.2",
|
||||||
@@ -34,12 +32,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@macalinao/eslint-config": "catalog:",
|
"@macalinao/eslint-config": "catalog:",
|
||||||
"@macalinao/tsconfig": "catalog:",
|
"@macalinao/tsconfig": "catalog:",
|
||||||
|
"@reviq/test-helpers": "workspace:*",
|
||||||
"@reviq/virtual-authenticator": "workspace:*",
|
"@reviq/virtual-authenticator": "workspace:*",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/pg": "^8.16.0",
|
|
||||||
"@types/zxcvbn": "^4.4.5",
|
"@types/zxcvbn": "^4.4.5",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"pg": "^8.16.3",
|
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
}
|
}
|
||||||
|
|||||||
1947
apps/api-server/src/__tests__/e2e/admin.test.ts
Normal file
1947
apps/api-server/src/__tests__/e2e/admin.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1844
apps/api-server/src/__tests__/e2e/orgs.test.ts
Normal file
1844
apps/api-server/src/__tests__/e2e/orgs.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -52,17 +52,28 @@ export async function signupWithPassword(
|
|||||||
// Hash password
|
// Hash password
|
||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
|
|
||||||
// Create user
|
// Create user (handle race condition if concurrent signup with same email)
|
||||||
const user = await db
|
try {
|
||||||
.insertInto("users")
|
const user = await db
|
||||||
.values({
|
.insertInto("users")
|
||||||
email,
|
.values({
|
||||||
password_hash: passwordHash,
|
email,
|
||||||
})
|
password_hash: passwordHash,
|
||||||
.returning(["id"])
|
})
|
||||||
.executeTakeFirstOrThrow();
|
.returning(["id"])
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
return user.id;
|
return user.id;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle duplicate email (unique constraint violation)
|
||||||
|
// Use generic error to prevent email enumeration
|
||||||
|
if (error instanceof Error && error.message.includes("users_email_key")) {
|
||||||
|
throw new ORPCError("BAD_REQUEST", {
|
||||||
|
message: "Unable to create account",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,55 +157,66 @@ export async function signupWithPasskey(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user and passkey in a transaction
|
// Create user and passkey in a transaction (handle race condition if concurrent signup)
|
||||||
const result = await db.transaction().execute(async (trx) => {
|
try {
|
||||||
// Create user
|
const result = await db.transaction().execute(async (trx) => {
|
||||||
const user = await trx
|
// Create user
|
||||||
.insertInto("users")
|
const user = await trx
|
||||||
.values({
|
.insertInto("users")
|
||||||
email,
|
.values({
|
||||||
password_hash: null,
|
email,
|
||||||
})
|
password_hash: null,
|
||||||
.returning(["id"])
|
})
|
||||||
.executeTakeFirstOrThrow();
|
.returning(["id"])
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
const newUserId = user.id;
|
const newUserId = user.id;
|
||||||
|
|
||||||
// Get friendly name from AAGUID
|
// Get friendly name from AAGUID
|
||||||
const guidName = KNOWN_AAGUIDS[registrationInfo.aaguid];
|
const guidName = KNOWN_AAGUIDS[registrationInfo.aaguid];
|
||||||
const passkeyName = guidName ?? "Default";
|
const passkeyName = guidName ?? "Default";
|
||||||
|
|
||||||
// Store the passkey
|
// Store the passkey
|
||||||
const { credential, credentialDeviceType, credentialBackedUp } =
|
const { credential, credentialDeviceType, credentialBackedUp } =
|
||||||
registrationInfo;
|
registrationInfo;
|
||||||
|
|
||||||
await trx
|
await trx
|
||||||
.insertInto("passkeys")
|
.insertInto("passkeys")
|
||||||
.values({
|
.values({
|
||||||
user_id: newUserId,
|
user_id: newUserId,
|
||||||
credential_id: Buffer.from(credential.id, "base64url"),
|
credential_id: Buffer.from(credential.id, "base64url"),
|
||||||
public_key: Buffer.from(credential.publicKey),
|
public_key: Buffer.from(credential.publicKey),
|
||||||
webauthn_user_id: options.user.id,
|
webauthn_user_id: options.user.id,
|
||||||
counter: BigInt(credential.counter),
|
counter: BigInt(credential.counter),
|
||||||
device_type: credentialDeviceType as "singleDevice" | "multiDevice",
|
device_type: credentialDeviceType as "singleDevice" | "multiDevice",
|
||||||
backup_eligible: registrationInfo.credentialBackedUp,
|
backup_eligible: registrationInfo.credentialBackedUp,
|
||||||
backup_status: credentialBackedUp,
|
backup_status: credentialBackedUp,
|
||||||
transports: JSON.stringify(response.response.transports ?? []),
|
transports: JSON.stringify(response.response.transports ?? []),
|
||||||
rpid: rpInfo.rpID,
|
rpid: rpInfo.rpID,
|
||||||
name: passkeyName,
|
name: passkeyName,
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// Delete the challenge
|
// Delete the challenge
|
||||||
await trx
|
await trx
|
||||||
.deleteFrom("webauthn_challenges")
|
.deleteFrom("webauthn_challenges")
|
||||||
.where("id", "=", String(challengeId))
|
.where("id", "=", String(challengeId))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
return { userId: newUserId };
|
return { userId: newUserId };
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.userId;
|
return result.userId;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle duplicate email (unique constraint violation)
|
||||||
|
// Use generic error to prevent email enumeration
|
||||||
|
if (error instanceof Error && error.message.includes("users_email_key")) {
|
||||||
|
throw new ORPCError("BAD_REQUEST", {
|
||||||
|
message: "Unable to create account",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,7 +263,7 @@ export const signup = os.auth.signup.handler(async ({ input, context }) => {
|
|||||||
);
|
);
|
||||||
userId = await signupWithPasskey(context.db, email, passkeyInfo, rpInfo);
|
userId = await signupWithPasskey(context.db, email, passkeyInfo, rpInfo);
|
||||||
} else {
|
} else {
|
||||||
// Should never reach here due to schema validation
|
// Unreachable - schema validation requires password or passkeyInfo
|
||||||
throw new ORPCError("BAD_REQUEST", {
|
throw new ORPCError("BAD_REQUEST", {
|
||||||
message: "Either password or passkeyInfo is required",
|
message: "Either password or passkeyInfo is required",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -115,10 +115,11 @@ export async function countOwners(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
const result = await db
|
const result = await db
|
||||||
.selectFrom("org_members")
|
.selectFrom("org_members")
|
||||||
.select((eb) => eb.fn.countAll<number>().as("count"))
|
.select((eb) => eb.fn.countAll().as("count"))
|
||||||
.where("org_id", "=", orgId)
|
.where("org_id", "=", orgId)
|
||||||
.where("role", "=", "owner")
|
.where("role", "=", "owner")
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
|
|
||||||
return result.count;
|
// PostgreSQL COUNT returns bigint (string), convert to number
|
||||||
|
return Number(result.count);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint . --cache",
|
"lint": "eslint . --cache",
|
||||||
"clean": "rm -rf dist .eslintcache",
|
"clean": "rm -rf dist .eslintcache",
|
||||||
"test": "bun test"
|
"test": "bun test src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "^2.0.1",
|
"@noble/hashes": "^2.0.1",
|
||||||
|
|||||||
25
bun.lock
25
bun.lock
@@ -35,12 +35,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@macalinao/eslint-config": "catalog:",
|
"@macalinao/eslint-config": "catalog:",
|
||||||
"@macalinao/tsconfig": "catalog:",
|
"@macalinao/tsconfig": "catalog:",
|
||||||
|
"@reviq/test-helpers": "workspace:*",
|
||||||
"@reviq/virtual-authenticator": "workspace:*",
|
"@reviq/virtual-authenticator": "workspace:*",
|
||||||
"@types/bun": "catalog:",
|
"@types/bun": "catalog:",
|
||||||
"@types/pg": "^8.16.0",
|
|
||||||
"@types/zxcvbn": "^4.4.5",
|
"@types/zxcvbn": "^4.4.5",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"pg": "^8.16.3",
|
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -192,6 +191,24 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/testing/test-helpers": {
|
||||||
|
"name": "@reviq/test-helpers",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@reviq/db": "workspace:*",
|
||||||
|
"@reviq/db-schema": "workspace:*",
|
||||||
|
"kysely": "^0.28.2",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@macalinao/eslint-config": "catalog:",
|
||||||
|
"@macalinao/tsconfig": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/testing/virtual-authenticator": {
|
"packages/testing/virtual-authenticator": {
|
||||||
"name": "@reviq/virtual-authenticator",
|
"name": "@reviq/virtual-authenticator",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@@ -201,7 +218,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@macalinao/eslint-config": "catalog:",
|
"@macalinao/eslint-config": "catalog:",
|
||||||
"@macalinao/tsconfig": "catalog:",
|
"@macalinao/tsconfig": "catalog:",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
@@ -439,6 +456,8 @@
|
|||||||
|
|
||||||
"@reviq/frontend-utils": ["@reviq/frontend-utils@workspace:packages/frontend-utils"],
|
"@reviq/frontend-utils": ["@reviq/frontend-utils@workspace:packages/frontend-utils"],
|
||||||
|
|
||||||
|
"@reviq/test-helpers": ["@reviq/test-helpers@workspace:packages/testing/test-helpers"],
|
||||||
|
|
||||||
"@reviq/utils": ["@reviq/utils@workspace:packages/utils"],
|
"@reviq/utils": ["@reviq/utils@workspace:packages/utils"],
|
||||||
|
|
||||||
"@reviq/virtual-authenticator": ["@reviq/virtual-authenticator@workspace:packages/testing/virtual-authenticator"],
|
"@reviq/virtual-authenticator": ["@reviq/virtual-authenticator@workspace:packages/testing/virtual-authenticator"],
|
||||||
|
|||||||
5
bunfig.toml
Normal file
5
bunfig.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[test]
|
||||||
|
coveragePathIgnorePatterns = [
|
||||||
|
"**/dist/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
]
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
\restrict F9AizESreuRieL4inRcHWWg3hyNET0FgnBDFBBBU3cZGPEpHjb591l8S2iglpap
|
|
||||||
|
|
||||||
-- Dumped from database version 17.7
|
-- Dumped from database version 17.7
|
||||||
-- Dumped by pg_dump version 17.7
|
-- Dumped by pg_dump version 17.7
|
||||||
@@ -1084,7 +1083,6 @@ ALTER TABLE ONLY public.user_devices
|
|||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
\unrestrict F9AizESreuRieL4inRcHWWg3hyNET0FgnBDFBBBU3cZGPEpHjb591l8S2iglpap
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
"typecheck": "turbo typecheck",
|
"typecheck": "turbo typecheck",
|
||||||
"clean": "turbo clean",
|
"clean": "turbo clean",
|
||||||
"test": "turbo test",
|
"test": "turbo test",
|
||||||
|
"test:unit": "SKIP_DB_TESTS=1 turbo test",
|
||||||
|
"test:all": "turbo test",
|
||||||
|
"test:cov": "bun test --coverage",
|
||||||
|
"test:unit:cov": "SKIP_DB_TESTS=1 bun test --coverage",
|
||||||
"db:codegen": "bun run --cwd packages/db-schema generate"
|
"db:codegen": "bun run --cwd packages/db-schema generate"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "bun test",
|
"test": "bun test src/",
|
||||||
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
||||||
"lint": "eslint . --cache"
|
"lint": "eslint . --cache"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
||||||
"lint": "eslint . --cache",
|
"lint": "eslint . --cache",
|
||||||
"test": "bun test"
|
"test": "bun test src/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@macalinao/eslint-config": "catalog:",
|
"@macalinao/eslint-config": "catalog:",
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"composite": true,
|
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"types": ["node", "bun"]
|
"types": ["node", "bun"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
12
packages/testing/test-helpers/eslint.config.js
Normal file
12
packages/testing/test-helpers/eslint.config.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { configs } from "@macalinao/eslint-config";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...configs.fast,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
33
packages/testing/test-helpers/package.json
Normal file
33
packages/testing/test-helpers/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "@reviq/test-helpers",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
||||||
|
"lint": "eslint . --cache"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@reviq/db": "workspace:*",
|
||||||
|
"@reviq/db-schema": "workspace:*",
|
||||||
|
"kysely": "^0.28.2",
|
||||||
|
"pg": "^8.16.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@macalinao/eslint-config": "catalog:",
|
||||||
|
"@macalinao/tsconfig": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
|
"eslint": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/testing/test-helpers/src/index.ts
Normal file
18
packages/testing/test-helpers/src/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export { describeE2E, SKIP_DB_TESTS } from "./skip-db-tests.js";
|
||||||
|
export {
|
||||||
|
DEFAULT_TEST_AAGUID,
|
||||||
|
KNOWN_AAGUIDS,
|
||||||
|
TEST_RP,
|
||||||
|
} from "./test-constants.js";
|
||||||
|
export {
|
||||||
|
createTestDb,
|
||||||
|
createTestUser,
|
||||||
|
destroySharedDb,
|
||||||
|
destroyTestDb,
|
||||||
|
getSharedDb,
|
||||||
|
getTestDatabaseUrl,
|
||||||
|
initTestDb,
|
||||||
|
runMigrations,
|
||||||
|
truncateAllTables,
|
||||||
|
} from "./test-db.js";
|
||||||
|
export { withTestTransaction } from "./test-transaction.js";
|
||||||
18
packages/testing/test-helpers/src/skip-db-tests.ts
Normal file
18
packages/testing/test-helpers/src/skip-db-tests.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe } from "bun:test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip flag for database-dependent tests.
|
||||||
|
* Set SKIP_DB_TESTS=1 to skip e2e tests that require a database.
|
||||||
|
*/
|
||||||
|
export const SKIP_DB_TESTS: boolean = process.env.SKIP_DB_TESTS === "1";
|
||||||
|
|
||||||
|
const _describeSkipIf = describe.skipIf(SKIP_DB_TESTS);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use for describe blocks that require database access.
|
||||||
|
* Automatically prefixes name with [e2e].
|
||||||
|
* Skips tests when SKIP_DB_TESTS=1 is set.
|
||||||
|
*/
|
||||||
|
export function describeE2E(name: string, fn: () => void): void {
|
||||||
|
_describeSkipIf(`[e2e] ${name}`, fn);
|
||||||
|
}
|
||||||
@@ -64,20 +64,31 @@ export function getTestDatabaseUrl(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a postgres URL to extract components
|
* Parses a postgres URL to extract components.
|
||||||
|
* Supports both TCP and unix socket connections.
|
||||||
|
*
|
||||||
|
* Unix socket URL format: postgresql:///dbname?host=/var/run/postgresql
|
||||||
*/
|
*/
|
||||||
function parsePostgresUrl(url: string): {
|
function parsePostgresUrl(url: string): {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number | undefined;
|
||||||
user: string;
|
user: string;
|
||||||
password: string;
|
password: string;
|
||||||
database: string;
|
database: string;
|
||||||
} {
|
} {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
|
|
||||||
|
// Unix socket: hostname is empty, socket path in `host` query param
|
||||||
|
const isUnixSocket = !parsed.hostname;
|
||||||
|
const socketPath = parsed.searchParams.get("host");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
host: parsed.hostname,
|
host: isUnixSocket
|
||||||
port: Number.parseInt(parsed.port || "5432", 10),
|
? (socketPath ?? "/var/run/postgresql")
|
||||||
user: parsed.username,
|
: parsed.hostname,
|
||||||
|
port: isUnixSocket ? undefined : Number.parseInt(parsed.port || "5432", 10),
|
||||||
|
// eslint-disable-next-line turbo/no-undeclared-env-vars, @typescript-eslint/prefer-nullish-coalescing -- USER is a system env var, and we want empty string to fall back
|
||||||
|
user: parsed.username || process.env.USER || "postgres",
|
||||||
password: parsed.password,
|
password: parsed.password,
|
||||||
database: parsed.pathname.slice(1), // Remove leading /
|
database: parsed.pathname.slice(1), // Remove leading /
|
||||||
};
|
};
|
||||||
6
packages/testing/test-helpers/tsconfig.json
Normal file
6
packages/testing/test-helpers/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["bun"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,19 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
||||||
"lint": "eslint . --cache",
|
"lint": "eslint . --cache",
|
||||||
"test": "bun test"
|
"test": "bun test src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/types": "^12.0.0"
|
"@simplewebauthn/types": "^12.0.0"
|
||||||
@@ -18,7 +23,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@macalinao/eslint-config": "catalog:",
|
"@macalinao/eslint-config": "catalog:",
|
||||||
"@macalinao/tsconfig": "catalog:",
|
"@macalinao/tsconfig": "catalog:",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "catalog:",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
"eslint": "catalog:",
|
"eslint": "catalog:",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
"extends": "@macalinao/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"composite": true,
|
|
||||||
"types": ["node", "bun"]
|
"types": ["node", "bun"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
"clean": "tsc --build --clean && rm -rf dist/ node_modules/ .eslintcache",
|
||||||
"lint": "eslint . --cache",
|
"lint": "eslint . --cache",
|
||||||
"test": "bun test"
|
"test": "bun test src/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20250529.0",
|
"@cloudflare/workers-types": "^4.20250529.0",
|
||||||
|
|||||||
16
scripts/db-dump
Executable file
16
scripts/db-dump
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Wrapper for dbmate dump that strips PostgreSQL's \restrict lines.
|
||||||
|
# PostgreSQL 17.6+ adds random \restrict/\unrestrict tokens to pg_dump output
|
||||||
|
# (CVE-2025-8714 security fix), causing schema.sql to change on every dump.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCHEMA_FILE="${DBMATE_SCHEMA_FILE:-./db/schema.sql}"
|
||||||
|
|
||||||
|
dbmate dump "$@"
|
||||||
|
|
||||||
|
# Strip \restrict and \unrestrict lines (they start with backslash)
|
||||||
|
if [[ -f "$SCHEMA_FILE" ]]; then
|
||||||
|
grep -v '^\\' "$SCHEMA_FILE" > "${SCHEMA_FILE}.tmp"
|
||||||
|
mv "${SCHEMA_FILE}.tmp" "$SCHEMA_FILE"
|
||||||
|
fi
|
||||||
16
scripts/db-migrate
Executable file
16
scripts/db-migrate
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Wrapper for dbmate migrate that strips PostgreSQL's \restrict lines.
|
||||||
|
# PostgreSQL 17.6+ adds random \restrict/\unrestrict tokens to pg_dump output
|
||||||
|
# (CVE-2025-8714 security fix), causing schema.sql to change on every dump.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCHEMA_FILE="${DBMATE_SCHEMA_FILE:-./db/schema.sql}"
|
||||||
|
|
||||||
|
dbmate migrate "$@"
|
||||||
|
|
||||||
|
# Strip \restrict and \unrestrict lines (they start with backslash)
|
||||||
|
if [[ -f "$SCHEMA_FILE" ]]; then
|
||||||
|
grep -v '^\\' "$SCHEMA_FILE" > "${SCHEMA_FILE}.tmp"
|
||||||
|
mv "${SCHEMA_FILE}.tmp" "$SCHEMA_FILE"
|
||||||
|
fi
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
ruleDirs:
|
ruleDirs:
|
||||||
- /Users/igm/proj/reviq/publisher-dashboard/.ast-grep/rules/
|
- .ast-grep/rules/
|
||||||
testConfigs:
|
testConfigs:
|
||||||
- testDir: /Users/igm/proj/reviq/publisher-dashboard/.ast-grep/rule-tests/
|
- testDir: .ast-grep/rule-tests/
|
||||||
utilDirs:
|
utilDirs:
|
||||||
- /Users/igm/proj/reviq/publisher-dashboard/.ast-grep/utils/
|
- .ast-grep/utils/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
"globalEnv": ["DATABASE_URL", "PORT"],
|
"globalEnv": ["DATABASE_URL", "PORT", "TEST_DATABASE_URL"],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"inputs": ["src/**/*.ts", "src/**/*.test.ts"],
|
"inputs": ["src/**/*.ts", "src/**/*.test.ts"],
|
||||||
|
"env": ["SKIP_DB_TESTS", "TEST_DATABASE_URL"],
|
||||||
"cache": false
|
"cache": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user