feat(api): Postgres persistence for registries + scans (owner-scoped, fail-closed)
CI (SIAX Cloud) / security (push) Successful in 13s
CI (SIAX Cloud) / contracts (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 13s
CI (SIAX Cloud) / contracts (push) Successful in 16s
CI (SIAX Cloud) / quality (push) Successful in 52s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m13s
CI (SIAX Cloud) / security (push) Successful in 13s
CI (SIAX Cloud) / contracts (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 13s
CI (SIAX Cloud) / contracts (push) Successful in 16s
CI (SIAX Cloud) / quality (push) Successful in 52s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m13s
- schema migration 001 (registries, scans; owner_sub = introspected subject) - pg pool + services; scan ownership enforced in SQL (INSERT ... SELECT WHERE owner_sub) - real handlers: 201/400/404/500, owner-scoped GET/POST, listScans registryId filter - graceful shutdown closes pool; 7 new persistence tests (21/21)
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
-- C0PY 001 — registries + scans (canonical capture target registry)
|
||||
-- Idempotent; owner_sub is the Zitadel introspection subject (never client-controlled).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS registries (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_sub TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS registries_owner_idx ON registries(owner_sub);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS scans (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
registry_id UUID NOT NULL REFERENCES registries(id) ON DELETE CASCADE,
|
||||
owner_sub TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
result JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS scans_owner_idx ON scans(owner_sub);
|
||||
CREATE INDEX IF NOT EXISTS scans_registry_idx ON scans(registry_id);
|
||||
@@ -11,14 +11,16 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@siax/c0py-core": "workspace:*",
|
||||
"@siax/c0py-config": "workspace:*",
|
||||
"@siax/c0py-core": "workspace:*",
|
||||
"@siax/c0py-types": "workspace:*",
|
||||
"fastify": "^5.12.0",
|
||||
"pg": "^8.23.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pg": "^8.23.1",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^3.0.0"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Pool } from "pg";
|
||||
|
||||
let pool: Pool | null = null;
|
||||
|
||||
export function getPool(): Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30_000,
|
||||
connectionTimeoutMillis: 5_000,
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function closePool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
}
|
||||
+100
-18
@@ -1,6 +1,13 @@
|
||||
import Fastify from "fastify";
|
||||
import Fastify, { type FastifyReply, type FastifyRequest } from "fastify";
|
||||
import { envSchema } from "@siax/c0py-config";
|
||||
import { createIntrospector } from "./auth/introspection.js";
|
||||
import { getPool, closePool } from "./db/pool.js";
|
||||
import {
|
||||
listRegistries,
|
||||
createRegistry,
|
||||
getRegistry,
|
||||
} from "./services/registries.js";
|
||||
import { createScan, listScans, getScan } from "./services/scans.js";
|
||||
|
||||
async function main() {
|
||||
const env = envSchema.parse(process.env);
|
||||
@@ -51,40 +58,115 @@ async function main() {
|
||||
}
|
||||
});
|
||||
|
||||
server.get("/", async () => ({
|
||||
name: "c0py",
|
||||
version: "0.1.0",
|
||||
status: "running",
|
||||
}));
|
||||
const requireSub = (request: FastifyRequest): string => {
|
||||
const user = (request as unknown as { user?: { sub?: string } }).user;
|
||||
if (!user?.sub) {
|
||||
throw Object.assign(new Error("missing subject"), { statusCode: 401 });
|
||||
}
|
||||
return user.sub;
|
||||
};
|
||||
|
||||
// Registry endpoints
|
||||
server.get("/v1/c0py/registries", async () => {
|
||||
return { registries: [] };
|
||||
const dbError = (reply: FastifyReply, err: unknown) => {
|
||||
reply.log.error(err, "database error");
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
};
|
||||
|
||||
// Registry endpoints (owner-scoped by introspected subject)
|
||||
server.get("/v1/c0py/registries", async (request, reply) => {
|
||||
try {
|
||||
return { registries: await listRegistries(getPool(), requireSub(request)) };
|
||||
} catch (err) {
|
||||
return dbError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
server.post("/v1/c0py/registries", async (request) => {
|
||||
server.post("/v1/c0py/registries", async (request, reply) => {
|
||||
const body = (request.body as Record<string, unknown>) ?? {};
|
||||
return { created: body };
|
||||
const name = typeof body.name === "string" ? body.name.trim() : "";
|
||||
const url = typeof body.url === "string" ? body.url.trim() : "";
|
||||
if (!name || !url) {
|
||||
await reply.code(400).send({ error: "name and url are required" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const registry = await createRegistry(getPool(), requireSub(request), {
|
||||
name,
|
||||
url,
|
||||
config: body.config,
|
||||
});
|
||||
await reply.code(201).send({ registry });
|
||||
} catch (err) {
|
||||
return dbError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
server.get("/v1/c0py/registries/:id", async (request) => {
|
||||
server.get("/v1/c0py/registries/:id", async (request, reply) => {
|
||||
const { id } = request.params as Record<string, string>;
|
||||
return { id };
|
||||
try {
|
||||
const registry = await getRegistry(getPool(), requireSub(request), id);
|
||||
if (!registry) {
|
||||
await reply.code(404).send({ error: "Not Found" });
|
||||
return;
|
||||
}
|
||||
return { registry };
|
||||
} catch (err) {
|
||||
return dbError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
// Scan endpoints
|
||||
server.post("/v1/c0py/scans", async (request) => {
|
||||
// Scan endpoints (owner-scoped, registry ownership enforced in SQL)
|
||||
server.post("/v1/c0py/scans", async (request, reply) => {
|
||||
const body = (request.body as Record<string, unknown>) ?? {};
|
||||
return { scanId: "scan-" + Date.now(), created: body };
|
||||
const registryId = typeof body.registryId === "string" ? body.registryId.trim() : "";
|
||||
if (!registryId) {
|
||||
await reply.code(400).send({ error: "registryId is required" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const scan = await createScan(getPool(), requireSub(request), registryId);
|
||||
if (!scan) {
|
||||
await reply.code(404).send({ error: "Not Found" });
|
||||
return;
|
||||
}
|
||||
await reply.code(201).send({ scan });
|
||||
} catch (err) {
|
||||
return dbError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
server.get("/v1/c0py/scans/:id", async (request) => {
|
||||
server.get("/v1/c0py/scans", async (request, reply) => {
|
||||
const query = request.query as Record<string, string | undefined>;
|
||||
try {
|
||||
return { scans: await listScans(getPool(), requireSub(request), query.registryId) };
|
||||
} catch (err) {
|
||||
return dbError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
server.get("/v1/c0py/scans/:id", async (request, reply) => {
|
||||
const { id } = request.params as Record<string, string>;
|
||||
return { id };
|
||||
try {
|
||||
const scan = await getScan(getPool(), requireSub(request), id);
|
||||
if (!scan) {
|
||||
await reply.code(404).send({ error: "Not Found" });
|
||||
return;
|
||||
}
|
||||
return { scan };
|
||||
} catch (err) {
|
||||
return dbError(reply, err);
|
||||
}
|
||||
});
|
||||
|
||||
await server.listen({ port: env.PORT, host: "0.0.0.0" });
|
||||
console.log(`c0py API listening on port ${env.PORT}`);
|
||||
|
||||
for (const signal of ["SIGTERM", "SIGINT"] as const) {
|
||||
process.on(signal, async () => {
|
||||
await server.close();
|
||||
await closePool();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
export interface RegistryRow {
|
||||
id: string;
|
||||
owner_sub: string;
|
||||
name: string;
|
||||
url: string;
|
||||
config: unknown;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export interface PoolLike {
|
||||
query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
|
||||
}
|
||||
|
||||
function toRegistry(row: Record<string, unknown>): {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
config: unknown;
|
||||
createdAt: string;
|
||||
} {
|
||||
return {
|
||||
id: String(row.id),
|
||||
name: String(row.name),
|
||||
url: String(row.url),
|
||||
config: row.config,
|
||||
createdAt: new Date(row.created_at as string).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listRegistries(db: PoolLike, ownerSub: string) {
|
||||
const res = await db.query(
|
||||
"SELECT id, owner_sub, name, url, config, created_at FROM registries WHERE owner_sub = $1 ORDER BY created_at DESC",
|
||||
[ownerSub],
|
||||
);
|
||||
return res.rows.map(toRegistry);
|
||||
}
|
||||
|
||||
export async function createRegistry(
|
||||
db: PoolLike,
|
||||
ownerSub: string,
|
||||
input: { name: string; url: string; config?: unknown },
|
||||
) {
|
||||
const res = await db.query(
|
||||
"INSERT INTO registries (owner_sub, name, url, config) VALUES ($1, $2, $3, $4::jsonb) RETURNING id, owner_sub, name, url, config, created_at",
|
||||
[ownerSub, input.name, input.url, JSON.stringify(input.config ?? {})],
|
||||
);
|
||||
return toRegistry(res.rows[0]);
|
||||
}
|
||||
|
||||
export async function getRegistry(db: PoolLike, ownerSub: string, id: string) {
|
||||
const res = await db.query(
|
||||
"SELECT id, owner_sub, name, url, config, created_at FROM registries WHERE owner_sub = $1 AND id = $2",
|
||||
[ownerSub, id],
|
||||
);
|
||||
return res.rows[0] ? toRegistry(res.rows[0]) : null;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
export interface ScanRow {
|
||||
id: string;
|
||||
registry_id: string;
|
||||
owner_sub: string;
|
||||
status: string;
|
||||
result: unknown;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface PoolLike {
|
||||
query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
|
||||
}
|
||||
|
||||
function toScan(row: Record<string, unknown>): {
|
||||
id: string;
|
||||
registryId: string;
|
||||
status: string;
|
||||
result: unknown;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
} {
|
||||
return {
|
||||
id: String(row.id),
|
||||
registryId: String(row.registry_id),
|
||||
status: String(row.status),
|
||||
result: row.result,
|
||||
createdAt: new Date(row.created_at as string).toISOString(),
|
||||
updatedAt: new Date(row.updated_at as string).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createScan(
|
||||
db: PoolLike,
|
||||
ownerSub: string,
|
||||
registryId: string,
|
||||
): Promise<ReturnType<typeof toScan> | null> {
|
||||
// Ownership check happens in SQL: the scan may only reference a registry
|
||||
// owned by the same subject. Inserting otherwise returns no rows (fail-closed).
|
||||
const res = await db.query(
|
||||
`INSERT INTO scans (registry_id, owner_sub, status)
|
||||
SELECT id, $1, 'pending' FROM registries WHERE id = $2 AND owner_sub = $1
|
||||
RETURNING id, registry_id, owner_sub, status, result, created_at, updated_at`,
|
||||
[ownerSub, registryId],
|
||||
);
|
||||
return res.rows[0] ? toScan(res.rows[0]) : null;
|
||||
}
|
||||
|
||||
export async function listScans(db: PoolLike, ownerSub: string, registryId?: string) {
|
||||
if (registryId !== undefined) {
|
||||
const res = await db.query(
|
||||
"SELECT id, registry_id, owner_sub, status, result, created_at, updated_at FROM scans WHERE owner_sub = $1 AND registry_id = $2 ORDER BY created_at DESC",
|
||||
[ownerSub, registryId],
|
||||
);
|
||||
return res.rows.map(toScan);
|
||||
}
|
||||
const res = await db.query(
|
||||
"SELECT id, registry_id, owner_sub, status, result, created_at, updated_at FROM scans WHERE owner_sub = $1 ORDER BY created_at DESC",
|
||||
[ownerSub],
|
||||
);
|
||||
return res.rows.map(toScan);
|
||||
}
|
||||
|
||||
export async function getScan(db: PoolLike, ownerSub: string, id: string) {
|
||||
const res = await db.query(
|
||||
"SELECT id, registry_id, owner_sub, status, result, created_at, updated_at FROM scans WHERE owner_sub = $1 AND id = $2",
|
||||
[ownerSub, id],
|
||||
);
|
||||
return res.rows[0] ? toScan(res.rows[0]) : null;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { listRegistries, createRegistry, getRegistry } from "../src/services/registries.js";
|
||||
import { createScan, listScans, getScan } from "../src/services/scans.js";
|
||||
import type { PoolLike } from "../src/services/registries.js";
|
||||
|
||||
function fakePool(handlers: {
|
||||
query: (text: string, values?: unknown[]) => { rows: Record<string, unknown>[] };
|
||||
}): PoolLike & { queries: { text: string; values?: unknown[] }[] } {
|
||||
const queries: { text: string; values?: unknown[] }[] = [];
|
||||
return {
|
||||
queries,
|
||||
query(text, values) {
|
||||
queries.push({ text, values });
|
||||
return handlers.query(text, values);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ROW = {
|
||||
id: "11111111-1111-1111-1111-111111111111",
|
||||
owner_sub: "svc-user",
|
||||
name: "example",
|
||||
url: "https://example.com",
|
||||
config: {},
|
||||
created_at: new Date("2026-09-16T10:00:00Z"),
|
||||
};
|
||||
|
||||
describe("registries service", () => {
|
||||
it("listRegistries filters by owner", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [ROW] }) });
|
||||
const out = await listRegistries(db, "svc-user");
|
||||
expect(out).toHaveLength(1);
|
||||
expect(out[0]).toMatchObject({ id: ROW.id, name: "example", createdAt: "2026-09-16T10:00:00.000Z" });
|
||||
expect(db.queries[0].values).toEqual(["svc-user"]);
|
||||
expect(db.queries[0].text).toContain("owner_sub = $1");
|
||||
});
|
||||
|
||||
it("createRegistry inserts with owner and config jsonb", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [ROW] }) });
|
||||
const out = await createRegistry(db, "svc-user", { name: "example", url: "https://example.com" });
|
||||
expect(out.id).toBe(ROW.id);
|
||||
expect(db.queries[0].values?.[0]).toBe("svc-user");
|
||||
expect(db.queries[0].text).toContain("RETURNING");
|
||||
});
|
||||
|
||||
it("getRegistry returns null when not owner (fail-closed)", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [] }) });
|
||||
const out = await getRegistry(db, "other-user", ROW.id);
|
||||
expect(out).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("scans service", () => {
|
||||
const SCAN_ROW = {
|
||||
...ROW,
|
||||
registry_id: ROW.id,
|
||||
status: "pending",
|
||||
result: {},
|
||||
updated_at: ROW.created_at,
|
||||
};
|
||||
|
||||
it("createScan enforces registry ownership in SQL and returns null otherwise", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [] }) });
|
||||
const out = await createScan(db, "svc-user", ROW.id);
|
||||
expect(out).toBeNull();
|
||||
expect(db.queries[0].text).toContain("owner_sub = $1");
|
||||
});
|
||||
|
||||
it("createScan returns scan for owned registry", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [SCAN_ROW] }) });
|
||||
const out = await createScan(db, "svc-user", ROW.id);
|
||||
expect(out).toMatchObject({ id: SCAN_ROW.id, registryId: SCAN_ROW.id, status: "pending" });
|
||||
});
|
||||
|
||||
it("listScans supports optional registryId filter", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [SCAN_ROW] }) });
|
||||
await listScans(db, "svc-user");
|
||||
expect(db.queries[0].text).not.toContain("registry_id = $2");
|
||||
await listScans(db, "svc-user", SCAN_ROW.id);
|
||||
expect(db.queries[1].values).toEqual(["svc-user", SCAN_ROW.id]);
|
||||
});
|
||||
|
||||
it("getScan returns null for foreign scan", async () => {
|
||||
const db = fakePool({ query: () => ({ rows: [] }) });
|
||||
expect(await getScan(db, "other-user", SCAN_ROW.id)).toBeNull();
|
||||
});
|
||||
});
|
||||
Generated
+116
@@ -47,6 +47,9 @@ importers:
|
||||
fastify:
|
||||
specifier: ^5.12.0
|
||||
version: 5.12.5
|
||||
pg:
|
||||
specifier: ^8.23.0
|
||||
version: 8.23.0
|
||||
zod:
|
||||
specifier: ^3.24.0
|
||||
version: 3.25.76
|
||||
@@ -54,6 +57,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^24.0.0
|
||||
version: 24.13.5
|
||||
'@types/pg':
|
||||
specifier: ^8.23.1
|
||||
version: 8.23.1
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.23.13
|
||||
@@ -716,6 +722,9 @@ packages:
|
||||
'@types/node@24.13.5':
|
||||
resolution: {integrity: sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==}
|
||||
|
||||
'@types/pg@8.23.1':
|
||||
resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==}
|
||||
|
||||
'@types/react-dom@19.3.0':
|
||||
resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
|
||||
peerDependencies:
|
||||
@@ -1239,6 +1248,40 @@ packages:
|
||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
pg-cloudflare@1.4.0:
|
||||
resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
|
||||
|
||||
pg-connection-string@2.14.0:
|
||||
resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==}
|
||||
|
||||
pg-int8@1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
pg-pool@3.14.0:
|
||||
resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==}
|
||||
peerDependencies:
|
||||
pg: '>=8.0'
|
||||
|
||||
pg-protocol@1.16.0:
|
||||
resolution: {integrity: sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==}
|
||||
|
||||
pg-types@2.2.0:
|
||||
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pg@8.23.0:
|
||||
resolution: {integrity: sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
peerDependencies:
|
||||
pg-native: '>=3.0.1'
|
||||
peerDependenciesMeta:
|
||||
pg-native:
|
||||
optional: true
|
||||
|
||||
pgpass@1.0.5:
|
||||
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -1264,6 +1307,22 @@ packages:
|
||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postgres-array@2.0.0:
|
||||
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postgres-bytea@1.0.1:
|
||||
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-date@1.0.7:
|
||||
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1562,6 +1621,10 @@ packages:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1973,6 +2036,12 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/pg@8.23.1':
|
||||
dependencies:
|
||||
'@types/node': 24.13.5
|
||||
pg-protocol: 1.16.0
|
||||
pg-types: 2.2.0
|
||||
|
||||
'@types/react-dom@19.3.0(@types/react@19.3.0)':
|
||||
dependencies:
|
||||
'@types/react': 19.3.0
|
||||
@@ -2552,6 +2621,41 @@ snapshots:
|
||||
|
||||
pathval@2.0.1: {}
|
||||
|
||||
pg-cloudflare@1.4.0:
|
||||
optional: true
|
||||
|
||||
pg-connection-string@2.14.0: {}
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-pool@3.14.0(pg@8.23.0):
|
||||
dependencies:
|
||||
pg: 8.23.0
|
||||
|
||||
pg-protocol@1.16.0: {}
|
||||
|
||||
pg-types@2.2.0:
|
||||
dependencies:
|
||||
pg-int8: 1.0.1
|
||||
postgres-array: 2.0.0
|
||||
postgres-bytea: 1.0.1
|
||||
postgres-date: 1.0.7
|
||||
postgres-interval: 1.2.0
|
||||
|
||||
pg@8.23.0:
|
||||
dependencies:
|
||||
pg-connection-string: 2.14.0
|
||||
pg-pool: 3.14.0(pg@8.23.0)
|
||||
pg-protocol: 1.16.0
|
||||
pg-types: 2.2.0
|
||||
pgpass: 1.0.5
|
||||
optionalDependencies:
|
||||
pg-cloudflare: 1.4.0
|
||||
|
||||
pgpass@1.0.5:
|
||||
dependencies:
|
||||
split2: 4.2.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.7: {}
|
||||
@@ -2588,6 +2692,16 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postgres-array@2.0.0: {}
|
||||
|
||||
postgres-bytea@1.0.1: {}
|
||||
|
||||
postgres-date@1.0.7: {}
|
||||
|
||||
postgres-interval@1.2.0:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.9.7: {}
|
||||
@@ -2878,6 +2992,8 @@ snapshots:
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
Reference in New Issue
Block a user