@gencow/core 0.1.27 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +4 -0
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1557 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive.d.ts +13 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +2 -26
- package/dist/storage.js +19 -15
- package/dist/workflow-types.d.ts +3 -1
- package/package.json +8 -7
- package/src/document-types.ts +95 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/index.ts +66 -1
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive.ts +13 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +1 -2
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +29 -46
- package/src/workflow-types.ts +3 -1
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared PGlite + `fixtures/basic` schema seed + RLS app role for integration tests.
|
|
3
|
-
*/
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import type { InferSelectModel } from "drizzle-orm";
|
|
7
|
-
import { getTableName, sql } from "drizzle-orm";
|
|
8
|
-
import type { PgTable } from "drizzle-orm/pg-core";
|
|
9
|
-
import { PGlite } from "@electric-sql/pglite";
|
|
10
|
-
import { drizzle } from "drizzle-orm/pglite";
|
|
11
|
-
|
|
12
|
-
import type { UserIdentity } from "../../reactive.js";
|
|
13
|
-
import { news, tasks, user } from "../fixtures/basic/schema.js";
|
|
14
|
-
import {
|
|
15
|
-
createPgliteRlsAppRole,
|
|
16
|
-
DEFAULT_PGLITE_RLS_APP_ROLE,
|
|
17
|
-
setPgliteSessionRole,
|
|
18
|
-
} from "./pglite-rls-session.js";
|
|
19
|
-
import { loadAndApplyMigrations } from "./pglite-migrations.js";
|
|
20
|
-
import { fillPartialRowsForInsert } from "./seed-like-fill.js";
|
|
21
|
-
|
|
22
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
|
|
24
|
-
export const basicFixtureUsers = [
|
|
25
|
-
{ id: "us_000", name: "User 0", email: "user-0@s.com", emailVerified: true },
|
|
26
|
-
{ id: "us_001", name: "User 1", email: "user-1@s.com", emailVerified: true },
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
/** Stable ids/titles; overlaps across users for search / RLS tests. */
|
|
30
|
-
export const basicFixtureTasks = [
|
|
31
|
-
{
|
|
32
|
-
id: "tk-000",
|
|
33
|
-
userId: basicFixtureUsers[0].id,
|
|
34
|
-
done: false,
|
|
35
|
-
title: "Project Alpha — Q4 review prep",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: "tk-001",
|
|
39
|
-
userId: basicFixtureUsers[1].id,
|
|
40
|
-
done: true,
|
|
41
|
-
title: "Project Alpha — teammate handoff",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "tk-002",
|
|
45
|
-
userId: basicFixtureUsers[0].id,
|
|
46
|
-
done: false,
|
|
47
|
-
title: "Project Alpha — backlog grooming",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: "tk-003",
|
|
51
|
-
userId: basicFixtureUsers[0].id,
|
|
52
|
-
done: false,
|
|
53
|
-
title: "Quarterly planning — Q4",
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: "tk-004",
|
|
57
|
-
userId: basicFixtureUsers[1].id,
|
|
58
|
-
done: false,
|
|
59
|
-
title: "Project Beta — API docs",
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
id: "tk-005",
|
|
63
|
-
userId: basicFixtureUsers[1].id,
|
|
64
|
-
done: false,
|
|
65
|
-
title: "Project Gamma — research notes",
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
id: "tk-006",
|
|
69
|
-
userId: basicFixtureUsers[0].id,
|
|
70
|
-
done: false,
|
|
71
|
-
title: "Project Beta — spike",
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
/** No `ownerRls()` — two rows for user0 / user1 (no DB RLS on `news`). */
|
|
76
|
-
export const basicFixtureNews = [
|
|
77
|
-
{ id: "nw-000", userId: basicFixtureUsers[0].id, title: "owned by user0" },
|
|
78
|
-
{ id: "nw-001", userId: basicFixtureUsers[1].id, title: "owned by user1" },
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
export const basicUser0Identity = {
|
|
82
|
-
id: basicFixtureUsers[0].id,
|
|
83
|
-
email: basicFixtureUsers[0].email,
|
|
84
|
-
} satisfies UserIdentity;
|
|
85
|
-
|
|
86
|
-
export const basicUser1Identity = {
|
|
87
|
-
id: basicFixtureUsers[1].id,
|
|
88
|
-
email: basicFixtureUsers[1].email,
|
|
89
|
-
} satisfies UserIdentity;
|
|
90
|
-
|
|
91
|
-
export type BasicTaskRow = InferSelectModel<typeof tasks>;
|
|
92
|
-
export type BasicNewsRow = InferSelectModel<typeof news>;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Bootstrap user migrations, seed users/tasks as table owner, then `SET ROLE` to non-owner app role
|
|
96
|
-
* so PostgreSQL RLS policies apply (same as `rls-crud-basic.test.ts`).
|
|
97
|
-
*/
|
|
98
|
-
export async function createBasicRlsEnvironment(): Promise<{
|
|
99
|
-
client: PGlite;
|
|
100
|
-
db: ReturnType<typeof drizzle>;
|
|
101
|
-
taskRows: BasicTaskRow[];
|
|
102
|
-
}> {
|
|
103
|
-
const client = new PGlite();
|
|
104
|
-
await client.waitReady;
|
|
105
|
-
await loadAndApplyMigrations(client, join(__dirname, "../fixtures/basic/migrations"));
|
|
106
|
-
const db = drizzle({ client });
|
|
107
|
-
await db.insert(user).values(fillPartialRowsForInsert(user, basicFixtureUsers));
|
|
108
|
-
const taskRows = fillPartialRowsForInsert(tasks, basicFixtureTasks) as BasicTaskRow[];
|
|
109
|
-
await db.insert(tasks).values(taskRows);
|
|
110
|
-
const newsRows = fillPartialRowsForInsert(news, basicFixtureNews) as BasicNewsRow[];
|
|
111
|
-
await db.insert(news).values(newsRows);
|
|
112
|
-
await createPgliteRlsAppRole(client, {
|
|
113
|
-
roleName: DEFAULT_PGLITE_RLS_APP_ROLE,
|
|
114
|
-
});
|
|
115
|
-
await setPgliteSessionRole(client, DEFAULT_PGLITE_RLS_APP_ROLE);
|
|
116
|
-
return { client, db, taskRows };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Asserts `pg_class.relrowsecurity` is false (table was never `ENABLE ROW LEVEL SECURITY`). */
|
|
120
|
-
export async function assertTableRowLevelSecurityDisabled(
|
|
121
|
-
db: ReturnType<typeof drizzle>,
|
|
122
|
-
table: PgTable,
|
|
123
|
-
): Promise<void> {
|
|
124
|
-
const relname = getTableName(table);
|
|
125
|
-
const q = await db.execute(sql`
|
|
126
|
-
select c.relrowsecurity as rls_enabled
|
|
127
|
-
from pg_class c
|
|
128
|
-
join pg_namespace n on n.oid = c.relnamespace
|
|
129
|
-
where n.nspname = 'public' and c.relname = ${relname}
|
|
130
|
-
`);
|
|
131
|
-
const row = (q as unknown as { rows: { rls_enabled: boolean }[] }).rows[0];
|
|
132
|
-
if (row?.rls_enabled !== false) {
|
|
133
|
-
throw new Error(`expected relrowsecurity = false for public.${relname}, got ${JSON.stringify(row)}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import type { PGlite } from "@electric-sql/pglite";
|
|
4
|
-
|
|
5
|
-
function listMigrationSqlFiles(migrationsDir: string): string[] {
|
|
6
|
-
return readdirSync(migrationsDir)
|
|
7
|
-
.filter((f) => f.endsWith(".sql"))
|
|
8
|
-
.sort()
|
|
9
|
-
.map((f) => join(migrationsDir, f));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Apply Drizzle-generated SQL from a folder of `.sql` files (split on `--> statement-breakpoint`).
|
|
14
|
-
*/
|
|
15
|
-
export async function loadAndApplyMigrations(client: PGlite, migrationsDir: string): Promise<void> {
|
|
16
|
-
const files = listMigrationSqlFiles(migrationsDir);
|
|
17
|
-
if (files.length === 0) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
`No .sql migration files in ${migrationsDir} (generate migrations with your Drizzle workflow)`,
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
for (const filePath of files) {
|
|
23
|
-
const raw = readFileSync(filePath, "utf8");
|
|
24
|
-
const chunks = raw
|
|
25
|
-
.split(/--> statement-breakpoint/g)
|
|
26
|
-
.map((s) => s.trim())
|
|
27
|
-
.filter(Boolean);
|
|
28
|
-
for (const sqlChunk of chunks) {
|
|
29
|
-
await client.exec(sqlChunk);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
|
|
3
|
-
/** Default role used in PGlite tests when you need RLS to apply (session user must not own the tables). */
|
|
4
|
-
export const DEFAULT_PGLITE_RLS_APP_ROLE = "gencow_rls_app";
|
|
5
|
-
|
|
6
|
-
function quoteIdent(name: string): string {
|
|
7
|
-
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
|
|
8
|
-
throw new Error(`Invalid SQL identifier: ${name}`);
|
|
9
|
-
}
|
|
10
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* After migrations (and optional seed as the bootstrap user), create a non-superuser role and grant
|
|
15
|
-
* DML on `public` so queries run as **non-owner** → PostgreSQL applies RLS policies without
|
|
16
|
-
* `FORCE ROW LEVEL SECURITY`.
|
|
17
|
-
*
|
|
18
|
-
* Omits `GRANT CONNECT ON DATABASE` — PGlite’s default DB can be `template1` and that grant has
|
|
19
|
-
* caused engine errors; schema/table privileges are enough for the embedded single-connection case.
|
|
20
|
-
*
|
|
21
|
-
* Call {@link setPgliteSessionRole} on the same `PGlite` instance before running app queries.
|
|
22
|
-
*/
|
|
23
|
-
export async function createPgliteRlsAppRole(
|
|
24
|
-
client: PGlite,
|
|
25
|
-
options?: { roleName?: string },
|
|
26
|
-
): Promise<string> {
|
|
27
|
-
const roleName = options?.roleName ?? DEFAULT_PGLITE_RLS_APP_ROLE;
|
|
28
|
-
const role = quoteIdent(roleName);
|
|
29
|
-
|
|
30
|
-
await client.exec(`
|
|
31
|
-
CREATE ROLE ${role} LOGIN;
|
|
32
|
-
GRANT USAGE ON SCHEMA public TO ${role};
|
|
33
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${role};
|
|
34
|
-
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${role};
|
|
35
|
-
`);
|
|
36
|
-
|
|
37
|
-
return roleName;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Run follow-up queries in this session as the given role (must not be table owner so RLS applies).
|
|
42
|
-
* The bootstrap user must be allowed to `SET ROLE` (e.g. superuser, or `GRANT rls_role TO bootstrap`).
|
|
43
|
-
*/
|
|
44
|
-
export async function setPgliteSessionRole(client: PGlite, roleName: string): Promise<void> {
|
|
45
|
-
await client.exec(`SET ROLE ${quoteIdent(roleName)}`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Restore session user to the original login role (typically the PGlite bootstrap user). */
|
|
49
|
-
export async function resetPgliteSessionRole(client: PGlite): Promise<void> {
|
|
50
|
-
await client.exec("RESET ROLE");
|
|
51
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Merge explicit partial insert rows with drizzle-seed-style values for missing columns.
|
|
3
|
-
* Uses drizzle-seed's `SeedService.generatePossibleGenerators` so new schema columns get
|
|
4
|
-
* the same generators as `seed()` without listing every column in test fixtures.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
8
|
-
import type { InferInsertModel } from "drizzle-orm";
|
|
9
|
-
import type { PgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
10
|
-
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
11
|
-
import { SeedService } from "drizzle-seed";
|
|
12
|
-
|
|
13
|
-
type SeedSvc = InstanceType<typeof SeedService>;
|
|
14
|
-
type SeedTable = Parameters<SeedSvc["generatePossibleGenerators"]>[1][number];
|
|
15
|
-
type SeedColumn = SeedTable["columns"][number];
|
|
16
|
-
|
|
17
|
-
/** Fields drizzle-seed reads from ORM columns (mirrors `getPostgresInfo` in drizzle-seed). */
|
|
18
|
-
type PgColumnSeedExtras = PgColumn & {
|
|
19
|
-
size?: number;
|
|
20
|
-
baseColumn?: PgColumn;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function pgColumnForSeed(column: PgColumn): PgColumnSeedExtras {
|
|
24
|
-
return column as PgColumnSeedExtras;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function hashSeed(str: string): number {
|
|
28
|
-
let h = 0;
|
|
29
|
-
for (let i = 0; i < str.length; i++) {
|
|
30
|
-
h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
|
|
31
|
-
}
|
|
32
|
-
return h >>> 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function getTypeParams(sqlType: string): SeedColumn["typeParams"] {
|
|
36
|
-
const typeParams: Record<string, number> = {};
|
|
37
|
-
if (sqlType.includes("[")) {
|
|
38
|
-
const match = sqlType.match(/\[\w*]/g);
|
|
39
|
-
if (match) {
|
|
40
|
-
typeParams.dimensions = match.length;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (
|
|
44
|
-
sqlType.startsWith("numeric") ||
|
|
45
|
-
sqlType.startsWith("decimal") ||
|
|
46
|
-
sqlType.startsWith("double precision") ||
|
|
47
|
-
sqlType.startsWith("real")
|
|
48
|
-
) {
|
|
49
|
-
const match = sqlType.match(/\((\d+), *(\d+)\)/);
|
|
50
|
-
if (match) {
|
|
51
|
-
typeParams.precision = Number(match[1]);
|
|
52
|
-
typeParams.scale = Number(match[2]);
|
|
53
|
-
}
|
|
54
|
-
} else if (
|
|
55
|
-
sqlType.startsWith("varchar") ||
|
|
56
|
-
sqlType.startsWith("bpchar") ||
|
|
57
|
-
sqlType.startsWith("char") ||
|
|
58
|
-
sqlType.startsWith("bit") ||
|
|
59
|
-
sqlType.startsWith("time") ||
|
|
60
|
-
sqlType.startsWith("timestamp") ||
|
|
61
|
-
sqlType.startsWith("interval")
|
|
62
|
-
) {
|
|
63
|
-
const match = sqlType.match(/\((\d+)\)/);
|
|
64
|
-
if (match) {
|
|
65
|
-
typeParams.length = Number(match[1]);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return typeParams;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getAllBaseColumns(baseColumn: PgColumn): NonNullable<SeedColumn["baseColumn"]> {
|
|
72
|
-
const b = pgColumnForSeed(baseColumn);
|
|
73
|
-
return {
|
|
74
|
-
name: baseColumn.name,
|
|
75
|
-
columnType: baseColumn.getSQLType(),
|
|
76
|
-
typeParams: getTypeParams(baseColumn.getSQLType()),
|
|
77
|
-
dataType: baseColumn.dataType,
|
|
78
|
-
size: b.size,
|
|
79
|
-
hasDefault: baseColumn.hasDefault,
|
|
80
|
-
default: baseColumn.default,
|
|
81
|
-
enumValues: baseColumn.enumValues,
|
|
82
|
-
isUnique: baseColumn.isUnique,
|
|
83
|
-
notNull: baseColumn.notNull,
|
|
84
|
-
primary: baseColumn.primary,
|
|
85
|
-
baseColumn: b.baseColumn === undefined ? undefined : getAllBaseColumns(b.baseColumn),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Drizzle `PgTable` → drizzle-seed `Table` (same shape as `getPostgresInfo` in drizzle-seed).
|
|
91
|
-
*/
|
|
92
|
-
export function drizzlePgTableToSeedTable(pgTable: PgTable, schemaKey: string): SeedTable {
|
|
93
|
-
const colsMap = getTableColumns(pgTable) as Record<string, PgColumn>;
|
|
94
|
-
const tableConfig = getTableConfig(pgTable);
|
|
95
|
-
const columns: SeedColumn[] = Object.entries(colsMap).map(([tsName, column]) => {
|
|
96
|
-
const c = pgColumnForSeed(column);
|
|
97
|
-
return {
|
|
98
|
-
name: tsName,
|
|
99
|
-
columnType: column.getSQLType(),
|
|
100
|
-
typeParams: getTypeParams(column.getSQLType()),
|
|
101
|
-
dataType: column.dataType,
|
|
102
|
-
size: c.size,
|
|
103
|
-
hasDefault: column.hasDefault,
|
|
104
|
-
default: column.default,
|
|
105
|
-
enumValues: column.enumValues,
|
|
106
|
-
isUnique: column.isUnique,
|
|
107
|
-
notNull: column.notNull,
|
|
108
|
-
primary: column.primary,
|
|
109
|
-
generatedIdentityType: column.generatedIdentity?.type,
|
|
110
|
-
identity: column.generatedIdentity !== undefined,
|
|
111
|
-
baseColumn: c.baseColumn === undefined ? undefined : getAllBaseColumns(c.baseColumn),
|
|
112
|
-
};
|
|
113
|
-
});
|
|
114
|
-
const primaryKeys = Object.keys(colsMap).filter((k) => colsMap[k]!.primary);
|
|
115
|
-
const uniqueConstraints = tableConfig.uniqueConstraints.map((uc) =>
|
|
116
|
-
uc.columns.map((c) => (c as PgColumn).name),
|
|
117
|
-
);
|
|
118
|
-
return { name: schemaKey, columns, primaryKeys, uniqueConstraints };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* For each row, use explicit keys from `partialRows[i]`; for any other column, run the same
|
|
123
|
-
* generator drizzle-seed would use for that column type (same `generate({ i })` loop as seed).
|
|
124
|
-
*
|
|
125
|
-
* `schemaKey` for drizzle-seed defaults to `getTableName(pgTable)` (the SQL table name from
|
|
126
|
-
* `pgTable("name", …)`). Pass `options.schemaKey` only when you need a different seed namespace.
|
|
127
|
-
*/
|
|
128
|
-
export function fillPartialRowsForInsert<T extends PgTable>(
|
|
129
|
-
pgTable: T,
|
|
130
|
-
partialRows: Array<Partial<InferInsertModel<T>>>,
|
|
131
|
-
options?: { seed?: number; version?: number; schemaKey?: string },
|
|
132
|
-
): InferInsertModel<T>[] {
|
|
133
|
-
const schemaKey = options?.schemaKey ?? getTableName(pgTable);
|
|
134
|
-
const seedService = new SeedService();
|
|
135
|
-
const seedTable = drizzlePgTableToSeedTable(pgTable, schemaKey);
|
|
136
|
-
const colsMap = getTableColumns(pgTable) as Record<string, PgColumn>;
|
|
137
|
-
const tsNames = Object.keys(colsMap);
|
|
138
|
-
const baseSeed = options?.seed ?? 0;
|
|
139
|
-
const count = partialRows.length;
|
|
140
|
-
|
|
141
|
-
// Build generator map for all columns at once using the v1 API
|
|
142
|
-
const tablePossibleGens = seedService.generatePossibleGenerators(
|
|
143
|
-
"postgresql",
|
|
144
|
-
[seedTable],
|
|
145
|
-
[],
|
|
146
|
-
undefined,
|
|
147
|
-
{ version: options?.version ?? 2 },
|
|
148
|
-
);
|
|
149
|
-
const colGenMap: Record<string, ReturnType<SeedSvc["selectVersionOfGenerator"]>> = {};
|
|
150
|
-
for (const colGen of tablePossibleGens[0]!.columnsPossibleGenerators) {
|
|
151
|
-
if (colGen.generator !== undefined) {
|
|
152
|
-
colGenMap[colGen.columnName] = seedService.selectVersionOfGenerator(colGen.generator);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const generators: Record<string, { generate: (args: { i: number }) => unknown }> = {};
|
|
157
|
-
|
|
158
|
-
for (const tsName of tsNames) {
|
|
159
|
-
const drizzleCol = colsMap[tsName]!;
|
|
160
|
-
if (drizzleCol.generatedIdentity?.type === "always") {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const needsGen = partialRows.some((p) => !Object.hasOwn(p, tsName));
|
|
164
|
-
if (!needsGen) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const gen = colGenMap[tsName];
|
|
168
|
-
if (gen === undefined) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`[seed-like-fill] unsupported column type for ${schemaKey}.${tsName}: ${drizzleCol.getSQLType()}`,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
const pRNGSeed = baseSeed + hashSeed(`${schemaKey}.${tsName}`);
|
|
174
|
-
gen.init({ count, seed: pRNGSeed });
|
|
175
|
-
generators[tsName] = gen;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const out: InferInsertModel<T>[] = [];
|
|
179
|
-
for (let i = 0; i < count; i++) {
|
|
180
|
-
const partial = partialRows[i]!;
|
|
181
|
-
const row: Record<string, unknown> = {};
|
|
182
|
-
for (const tsName of tsNames) {
|
|
183
|
-
const drizzleCol = colsMap[tsName]!;
|
|
184
|
-
if (drizzleCol.generatedIdentity?.type === "always") {
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (Object.hasOwn(partial, tsName)) {
|
|
188
|
-
row[tsName] = partial[tsName as keyof typeof partial];
|
|
189
|
-
} else {
|
|
190
|
-
const g = generators[tsName];
|
|
191
|
-
if (!g) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`[seed-like-fill] missing generator for ${schemaKey}.${tsName} (partial row without value)`,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
row[tsName] = g.generate({ i });
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
out.push(row as InferInsertModel<T>);
|
|
200
|
-
}
|
|
201
|
-
return out;
|
|
202
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { drizzle } from "drizzle-orm/pglite";
|
|
2
|
-
|
|
3
|
-
import { createRlsDb } from "../../rls-db.js";
|
|
4
|
-
import type { GencowCtx, UserIdentity } from "../../reactive.js";
|
|
5
|
-
|
|
6
|
-
export function makeTestGencowCtxWithRls(db: ReturnType<typeof drizzle>, identity: UserIdentity): GencowCtx {
|
|
7
|
-
const scoped = createRlsDb(db, { userId: identity.id });
|
|
8
|
-
return {
|
|
9
|
-
db: scoped,
|
|
10
|
-
unsafeDb: db,
|
|
11
|
-
auth: {
|
|
12
|
-
getUserIdentity: () => identity,
|
|
13
|
-
requireAuth: () => identity,
|
|
14
|
-
},
|
|
15
|
-
storage: {} as GencowCtx["storage"],
|
|
16
|
-
scheduler: {} as GencowCtx["scheduler"],
|
|
17
|
-
realtime: { emit: () => {}, refresh: () => {} },
|
|
18
|
-
retry: async (fn) => fn(),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const FORCE_ROLLBACK = Symbol("force-rollback");
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Executes a callback inside one outer transaction and always rolls it back.
|
|
26
|
-
*
|
|
27
|
-
* This is useful for mutation tests: handlers can run multiple operations and
|
|
28
|
-
* see intermediate writes, but database state is discarded after the callback.
|
|
29
|
-
*/
|
|
30
|
-
export async function runWithRollbackTestGencowCtxWithRls<T>(
|
|
31
|
-
db: ReturnType<typeof drizzle>,
|
|
32
|
-
identity: UserIdentity,
|
|
33
|
-
run: (ctx: GencowCtx) => Promise<T>,
|
|
34
|
-
): Promise<T> {
|
|
35
|
-
let result!: T;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
await (db as any).transaction(async (outerTx: ReturnType<typeof drizzle>) => {
|
|
39
|
-
const txCtx = makeTestGencowCtxWithRls(outerTx, identity);
|
|
40
|
-
result = await run(txCtx);
|
|
41
|
-
throw FORCE_ROLLBACK;
|
|
42
|
-
});
|
|
43
|
-
} catch (error) {
|
|
44
|
-
if (error !== FORCE_ROLLBACK) {
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* packages/core/src/__tests__/httpaction.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests for httpAction() — custom HTTP endpoint registration.
|
|
5
|
-
*
|
|
6
|
-
* Run: bun test packages/core/src/__tests__/httpaction.test.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach } from "bun:test";
|
|
10
|
-
import { httpAction, getRegisteredHttpActions } from "../reactive.js";
|
|
11
|
-
|
|
12
|
-
// Clean up httpAction registry between tests
|
|
13
|
-
// Note: httpAction uses globalThis.__gencow_httpActionRegistry (push-only array)
|
|
14
|
-
// We clear it in beforeEach to isolate tests.
|
|
15
|
-
|
|
16
|
-
describe("httpAction()", () => {
|
|
17
|
-
const initialLength = getRegisteredHttpActions().length;
|
|
18
|
-
|
|
19
|
-
it("httpAction 등록 시 레지스트리에 추가된다", () => {
|
|
20
|
-
const before = getRegisteredHttpActions().length;
|
|
21
|
-
|
|
22
|
-
httpAction({
|
|
23
|
-
method: "GET",
|
|
24
|
-
path: "/test/health",
|
|
25
|
-
handler: async () => ({ body: { status: "ok" } }),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const after = getRegisteredHttpActions().length;
|
|
29
|
-
expect(after).toBe(before + 1);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("method, path가 정확히 설정된다", () => {
|
|
33
|
-
httpAction({
|
|
34
|
-
method: "POST",
|
|
35
|
-
path: "/webhook/stripe",
|
|
36
|
-
handler: async () => ({ body: { received: true } }),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const actions = getRegisteredHttpActions();
|
|
40
|
-
const last = actions[actions.length - 1];
|
|
41
|
-
expect(last.method).toBe("POST");
|
|
42
|
-
expect(last.path).toBe("/webhook/stripe");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("public 기본값은 false이다", () => {
|
|
46
|
-
httpAction({
|
|
47
|
-
method: "GET",
|
|
48
|
-
path: "/test/private",
|
|
49
|
-
handler: async () => ({ body: {} }),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const actions = getRegisteredHttpActions();
|
|
53
|
-
const last = actions[actions.length - 1];
|
|
54
|
-
expect(last.isPublic).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("public: true 설정 시 isPublic === true", () => {
|
|
58
|
-
httpAction({
|
|
59
|
-
method: "GET",
|
|
60
|
-
path: "/test/public-endpoint",
|
|
61
|
-
public: true,
|
|
62
|
-
handler: async () => ({ body: {} }),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const actions = getRegisteredHttpActions();
|
|
66
|
-
const last = actions[actions.length - 1];
|
|
67
|
-
expect(last.isPublic).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("handler가 HttpActionDef에 포함된다", () => {
|
|
71
|
-
const handler = async () => ({ body: { ok: true } });
|
|
72
|
-
httpAction({
|
|
73
|
-
method: "PUT",
|
|
74
|
-
path: "/test/with-handler",
|
|
75
|
-
handler,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const actions = getRegisteredHttpActions();
|
|
79
|
-
const last = actions[actions.length - 1];
|
|
80
|
-
expect(last.handler).toBe(handler);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("모든 HTTP 메서드 지원 (GET, POST, PUT, DELETE, PATCH)", () => {
|
|
84
|
-
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
|
|
85
|
-
const before = getRegisteredHttpActions().length;
|
|
86
|
-
|
|
87
|
-
for (const method of methods) {
|
|
88
|
-
httpAction({
|
|
89
|
-
method,
|
|
90
|
-
path: `/test/method-${method.toLowerCase()}`,
|
|
91
|
-
handler: async () => ({ body: {} }),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const after = getRegisteredHttpActions().length;
|
|
96
|
-
expect(after).toBe(before + 5);
|
|
97
|
-
|
|
98
|
-
const actions = getRegisteredHttpActions();
|
|
99
|
-
const registered = actions.slice(-5).map((a) => a.method);
|
|
100
|
-
expect(registered).toEqual(["GET", "POST", "PUT", "DELETE", "PATCH"]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("getRegisteredHttpActions()는 복사본을 반환한다 (원본 보호)", () => {
|
|
104
|
-
const a = getRegisteredHttpActions();
|
|
105
|
-
const b = getRegisteredHttpActions();
|
|
106
|
-
expect(a).not.toBe(b);
|
|
107
|
-
expect(a).toEqual(b);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("Hono 경로 패턴 (/api/:id) 지원", () => {
|
|
111
|
-
httpAction({
|
|
112
|
-
method: "GET",
|
|
113
|
-
path: "/api/apps/:id/status",
|
|
114
|
-
public: true,
|
|
115
|
-
handler: async () => ({ body: {} }),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const actions = getRegisteredHttpActions();
|
|
119
|
-
const last = actions[actions.length - 1];
|
|
120
|
-
expect(last.path).toBe("/api/apps/:id/status");
|
|
121
|
-
});
|
|
122
|
-
});
|