@stackbone/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3

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.
@@ -9,39 +9,30 @@ interface CreateTestDatabaseOptions {
9
9
  */
10
10
  migrationsDir: string;
11
11
  /**
12
- * Optional Postgres URL pointing at the shared dev DB. Falls back, in order,
13
- * to:
14
- * 1. `STACKBONE_TEST_POSTGRES_URL` (preferred lets CI override
15
- * independently of the agent's runtime URL).
16
- * 2. `STACKBONE_POSTGRES_URL` (the env var the rest of the SDK reads).
17
- * 3. `DATABASE_URL` (the env var loaded by `node --env-file=.env`).
18
- * 4. `postgres://stackbone:stackbone@localhost:5432/stackbone` (the
19
- * docker-compose default — the same DB `pnpm db:migrate` hits).
12
+ * Postgres image to spin up. Defaults to `pgvector/pgvector:pg17` so RAG
13
+ * and AI starters that need the `vector` extension work out of the box.
14
+ * Override with `postgres:17` for a vanilla minimum-footprint container,
15
+ * or with a custom tag (e.g. `stackbone/postgres:local`) when the test
16
+ * suite needs the bundled `pgmq` / `pg_cron` extensions.
20
17
  */
21
- connectionString?: string;
18
+ image?: string;
22
19
  }
23
20
  /** Result of `createTestDatabase`. */
24
21
  interface TestDatabase {
25
- /**
26
- * Postgres URL pinned to the throwaway schema via the `?schema=<name>` query
27
- * parameter. Useful for sub-processes (drizzle-kit studio, a worker) that
28
- * need to share the same isolation.
29
- */
22
+ /** Postgres URL of the running container, ready for sub-processes. */
30
23
  connectionString: string;
31
- /** Drizzle handle bound to the throwaway schema, ready to query. */
24
+ /** Drizzle handle bound to the container's database, ready to query. */
32
25
  drizzle: PostgresJsDatabase<Record<string, never>>;
33
26
  /**
34
- * Tears the test DB down: closes the pool, then drops the schema with
35
- * CASCADE so dependent tables/indexes/sequences vanish in one statement.
27
+ * Tears the test DB down: closes the pool and stops the container.
36
28
  * Idempotent — calling it twice is a no-op.
37
29
  */
38
30
  dispose: () => Promise<void>;
39
31
  }
40
32
  /**
41
- * Provisions an ephemeral, randomly-named schema inside the shared dev
42
- * Postgres, applies the supplied migrations directory against it, and hands
43
- * back a Drizzle instance + `dispose()` for tear-down. See module header for
44
- * design rationale.
33
+ * Spins up an ephemeral Postgres container, applies the supplied migrations
34
+ * directory against it, and hands back a Drizzle instance + `dispose()` for
35
+ * tear-down. See module header for design rationale.
45
36
  */
46
37
  declare const createTestDatabase: (options: CreateTestDatabaseOptions) => Promise<TestDatabase>;
47
38
 
@@ -9,39 +9,30 @@ interface CreateTestDatabaseOptions {
9
9
  */
10
10
  migrationsDir: string;
11
11
  /**
12
- * Optional Postgres URL pointing at the shared dev DB. Falls back, in order,
13
- * to:
14
- * 1. `STACKBONE_TEST_POSTGRES_URL` (preferred lets CI override
15
- * independently of the agent's runtime URL).
16
- * 2. `STACKBONE_POSTGRES_URL` (the env var the rest of the SDK reads).
17
- * 3. `DATABASE_URL` (the env var loaded by `node --env-file=.env`).
18
- * 4. `postgres://stackbone:stackbone@localhost:5432/stackbone` (the
19
- * docker-compose default — the same DB `pnpm db:migrate` hits).
12
+ * Postgres image to spin up. Defaults to `pgvector/pgvector:pg17` so RAG
13
+ * and AI starters that need the `vector` extension work out of the box.
14
+ * Override with `postgres:17` for a vanilla minimum-footprint container,
15
+ * or with a custom tag (e.g. `stackbone/postgres:local`) when the test
16
+ * suite needs the bundled `pgmq` / `pg_cron` extensions.
20
17
  */
21
- connectionString?: string;
18
+ image?: string;
22
19
  }
23
20
  /** Result of `createTestDatabase`. */
24
21
  interface TestDatabase {
25
- /**
26
- * Postgres URL pinned to the throwaway schema via the `?schema=<name>` query
27
- * parameter. Useful for sub-processes (drizzle-kit studio, a worker) that
28
- * need to share the same isolation.
29
- */
22
+ /** Postgres URL of the running container, ready for sub-processes. */
30
23
  connectionString: string;
31
- /** Drizzle handle bound to the throwaway schema, ready to query. */
24
+ /** Drizzle handle bound to the container's database, ready to query. */
32
25
  drizzle: PostgresJsDatabase<Record<string, never>>;
33
26
  /**
34
- * Tears the test DB down: closes the pool, then drops the schema with
35
- * CASCADE so dependent tables/indexes/sequences vanish in one statement.
27
+ * Tears the test DB down: closes the pool and stops the container.
36
28
  * Idempotent — calling it twice is a no-op.
37
29
  */
38
30
  dispose: () => Promise<void>;
39
31
  }
40
32
  /**
41
- * Provisions an ephemeral, randomly-named schema inside the shared dev
42
- * Postgres, applies the supplied migrations directory against it, and hands
43
- * back a Drizzle instance + `dispose()` for tear-down. See module header for
44
- * design rationale.
33
+ * Spins up an ephemeral Postgres container, applies the supplied migrations
34
+ * directory against it, and hands back a Drizzle instance + `dispose()` for
35
+ * tear-down. See module header for design rationale.
45
36
  */
46
37
  declare const createTestDatabase: (options: CreateTestDatabaseOptions) => Promise<TestDatabase>;
47
38
 
@@ -1,50 +1,18 @@
1
- import { randomBytes } from 'crypto';
1
+ import { PostgreSqlContainer } from '@testcontainers/postgresql';
2
2
  import { drizzle } from 'drizzle-orm/postgres-js';
3
3
  import { migrate } from 'drizzle-orm/postgres-js/migrator';
4
4
  import postgres from 'postgres';
5
5
 
6
6
  var __defProp = Object.defineProperty;
7
7
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
- var resolveAdminUrl = /* @__PURE__ */ __name((override) => override ?? process.env["STACKBONE_TEST_POSTGRES_URL"] ?? process.env["STACKBONE_POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://stackbone:stackbone@localhost:5432/stackbone", "resolveAdminUrl");
9
- var generateSchemaName = /* @__PURE__ */ __name(() => `stackbone_test_${randomBytes(8).toString("hex")}`, "generateSchemaName");
10
- var tagConnectionStringWithSchema = /* @__PURE__ */ __name((url, schemaName) => {
11
- const u = new URL(url);
12
- u.searchParams.set("schema", schemaName);
13
- return u.toString();
14
- }, "tagConnectionStringWithSchema");
15
- var dropSchema = /* @__PURE__ */ __name(async (adminUrl, schemaName) => {
16
- const admin = postgres(adminUrl, {
17
- max: 1,
18
- onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice")
19
- });
20
- try {
21
- await admin.unsafe(`DROP SCHEMA "${schemaName.replace(/"/g, '""')}" CASCADE`);
22
- } finally {
23
- await admin.end({
24
- timeout: 5
25
- });
26
- }
27
- }, "dropSchema");
8
+ var DEFAULT_IMAGE = "pgvector/pgvector:pg17";
28
9
  var createTestDatabase = /* @__PURE__ */ __name(async (options) => {
29
- const adminUrl = resolveAdminUrl(options.connectionString);
30
- const schemaName = generateSchemaName();
31
- const admin = postgres(adminUrl, {
32
- max: 1,
33
- onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice")
34
- });
35
- try {
36
- await admin`CREATE SCHEMA ${admin(schemaName)}`;
37
- } finally {
38
- await admin.end({
39
- timeout: 5
40
- });
41
- }
42
- const client = postgres(adminUrl, {
10
+ const image = options.image ?? DEFAULT_IMAGE;
11
+ const container = await new PostgreSqlContainer(image).withDatabase("stackbone_test").withUsername("stackbone").withPassword("stackbone").start();
12
+ const connectionString = container.getConnectionUri();
13
+ const client = postgres(connectionString, {
43
14
  max: 5,
44
- onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice"),
45
- connection: {
46
- search_path: schemaName
47
- }
15
+ onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice")
48
16
  });
49
17
  const db = drizzle(client);
50
18
  let disposed = false;
@@ -54,19 +22,20 @@ var createTestDatabase = /* @__PURE__ */ __name(async (options) => {
54
22
  await client.end({
55
23
  timeout: 5
56
24
  }).catch(() => void 0);
57
- await dropSchema(adminUrl, schemaName).catch(() => void 0);
25
+ await container.stop({
26
+ timeout: 5e3
27
+ }).catch(() => void 0);
58
28
  }, "dispose");
59
29
  try {
60
30
  await migrate(db, {
61
- migrationsFolder: options.migrationsDir,
62
- migrationsSchema: schemaName
31
+ migrationsFolder: options.migrationsDir
63
32
  });
64
33
  } catch (err) {
65
34
  await dispose();
66
35
  throw err;
67
36
  }
68
37
  return {
69
- connectionString: tagConnectionStringWithSchema(adminUrl, schemaName),
38
+ connectionString,
70
39
  drizzle: db,
71
40
  dispose
72
41
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../libs/sdk/src/db/testing/index.ts"],"names":["resolveAdminUrl","override","process","env","generateSchemaName","randomBytes","toString","tagConnectionStringWithSchema","url","schemaName","u","URL","searchParams","set","dropSchema","adminUrl","admin","postgres","max","onnotice","undefined","unsafe","replace","end","timeout","createTestDatabase","options","connectionString","client","connection","search_path","db","drizzle","disposed","dispose","catch","migrate","migrationsFolder","migrationsDir","migrationsSchema","err"],"mappings":"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","file":"index.js","sourcesContent":["// Public API for `@stackbone/sdk/db/testing`.\n//\n// `createTestDatabase` provisions an ephemeral, randomly-named Postgres schema\n// inside a shared developer database (the docker-compose `stackbone-postgres`\n// container in dev, or whatever Postgres the consumer points at via env or\n// `connectionString`) and applies a drizzle-kit-shaped migrations folder\n// against it. Returns a ready-to-query Drizzle instance plus a `dispose()`\n// that drops the schema and closes the pool — guaranteeing that two parallel\n// tests neither bleed rows nor leak schemas after the suite ends.\n//\n// Why a schema instead of a fresh database or a testcontainer:\n// - Schema creation is ~milliseconds (CREATE DATABASE on Postgres takes a\n// real lock and copies template1).\n// - One pre-pulled Postgres image (the dev compose container) avoids the\n// per-suite testcontainer boot cost (~3-5s) on every `nx test`.\n// - `search_path` isolation keeps every creator's migration SQL — which\n// references unqualified table names — applying to the throwaway schema\n// without rewriting their `.sql` files.\n//\n// Why not reuse `apps/cli/src/dev/db-migrator.ts`:\n// - That module owns the platform's advisory-lock + `__stackbone_migrations__`\n// journal table, which is shared across the whole DB and explicitly NOT\n// scoped to a schema. Tests run inside their own schema, so the standard\n// drizzle migrator (`drizzle-orm/postgres-js/migrator`) with\n// `migrationsSchema` pointed at the throwaway schema is the right primitive.\n\nimport { randomBytes } from 'node:crypto';\nimport { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';\nimport { migrate } from 'drizzle-orm/postgres-js/migrator';\nimport postgres, { type Sql } from 'postgres';\n\n/** Inputs for `createTestDatabase`. */\nexport interface CreateTestDatabaseOptions {\n /**\n * Drizzle-kit-shaped migrations directory: `meta/_journal.json` plus one\n * `<tag>.sql` file per entry. Required — the helper applies the migrations\n * before returning so the first query a test runs already sees the schema.\n */\n migrationsDir: string;\n /**\n * Optional Postgres URL pointing at the shared dev DB. Falls back, in order,\n * to:\n * 1. `STACKBONE_TEST_POSTGRES_URL` (preferred — lets CI override\n * independently of the agent's runtime URL).\n * 2. `STACKBONE_POSTGRES_URL` (the env var the rest of the SDK reads).\n * 3. `DATABASE_URL` (the env var loaded by `node --env-file=.env`).\n * 4. `postgres://stackbone:stackbone@localhost:5432/stackbone` (the\n * docker-compose default — the same DB `pnpm db:migrate` hits).\n */\n connectionString?: string;\n}\n\n/** Result of `createTestDatabase`. */\nexport interface TestDatabase {\n /**\n * Postgres URL pinned to the throwaway schema via the `?schema=<name>` query\n * parameter. Useful for sub-processes (drizzle-kit studio, a worker) that\n * need to share the same isolation.\n */\n connectionString: string;\n /** Drizzle handle bound to the throwaway schema, ready to query. */\n drizzle: PostgresJsDatabase<Record<string, never>>;\n /**\n * Tears the test DB down: closes the pool, then drops the schema with\n * CASCADE so dependent tables/indexes/sequences vanish in one statement.\n * Idempotent — calling it twice is a no-op.\n */\n dispose: () => Promise<void>;\n}\n\n/**\n * Resolves the admin Postgres URL. See `connectionString` on\n * `CreateTestDatabaseOptions` for the env-fallback chain.\n */\nconst resolveAdminUrl = (override?: string): string =>\n override ??\n process.env['STACKBONE_TEST_POSTGRES_URL'] ??\n process.env['STACKBONE_POSTGRES_URL'] ??\n process.env['DATABASE_URL'] ??\n 'postgres://stackbone:stackbone@localhost:5432/stackbone';\n\n/**\n * Generates a random schema name short enough to fit Postgres's 63-char\n * identifier limit while still being unique across parallel workers.\n */\nconst generateSchemaName = (): string => `stackbone_test_${randomBytes(8).toString('hex')}`;\n\n/**\n * Encodes a `?schema=<name>` query parameter into the URL so callers (and\n * sub-processes) can opt into the same isolation by reading their own URL.\n * Metadata for the caller — libpq itself does not honour it.\n */\nconst tagConnectionStringWithSchema = (url: string, schemaName: string): string => {\n const u = new URL(url);\n u.searchParams.set('schema', schemaName);\n return u.toString();\n};\n\n/** Drops the throwaway schema via a one-shot admin connection. */\nconst dropSchema = async (adminUrl: string, schemaName: string): Promise<void> => {\n const admin = postgres(adminUrl, { max: 1, onnotice: () => undefined });\n try {\n await admin.unsafe(`DROP SCHEMA \"${schemaName.replace(/\"/g, '\"\"')}\" CASCADE`);\n } finally {\n await admin.end({ timeout: 5 });\n }\n};\n\n/**\n * Provisions an ephemeral, randomly-named schema inside the shared dev\n * Postgres, applies the supplied migrations directory against it, and hands\n * back a Drizzle instance + `dispose()` for tear-down. See module header for\n * design rationale.\n */\nexport const createTestDatabase = async (\n options: CreateTestDatabaseOptions,\n): Promise<TestDatabase> => {\n const adminUrl = resolveAdminUrl(options.connectionString);\n const schemaName = generateSchemaName();\n\n // Step 1 — admin client to bootstrap the schema. Tiny pool because we only\n // run one DDL statement before tearing the connection down.\n const admin = postgres(adminUrl, { max: 1, onnotice: () => undefined });\n try {\n await admin`CREATE SCHEMA ${admin(schemaName)}`;\n } finally {\n await admin.end({ timeout: 5 });\n }\n\n // Step 2 — scoped client. `search_path` is set at connection time so every\n // statement (DDL from the migrations and the consumer's own queries) lands\n // inside the throwaway schema without qualifying table names.\n const client: Sql = postgres(adminUrl, {\n max: 5,\n onnotice: () => undefined,\n connection: { search_path: schemaName },\n });\n const db = drizzle(client);\n\n let disposed = false;\n const dispose = async (): Promise<void> => {\n if (disposed) return;\n disposed = true;\n await client.end({ timeout: 5 }).catch(() => undefined);\n await dropSchema(adminUrl, schemaName).catch(() => undefined);\n };\n\n // Step 3 — apply migrations. `migrationsSchema` keeps Drizzle's journal\n // table inside the throwaway schema too, so the schema-drop in `dispose`\n // wipes both creator-owned tables and Drizzle's own bookkeeping in one\n // statement. On failure, dispose so a broken fixture does not leak a\n // half-bootstrapped schema for the rest of the suite.\n try {\n await migrate(db, {\n migrationsFolder: options.migrationsDir,\n migrationsSchema: schemaName,\n });\n } catch (err) {\n await dispose();\n throw err;\n }\n\n return {\n connectionString: tagConnectionStringWithSchema(adminUrl, schemaName),\n drizzle: db,\n dispose,\n };\n};\n"]}
1
+ {"version":3,"sources":["../../../../../libs/sdk/src/db/testing/index.ts"],"names":["DEFAULT_IMAGE","createTestDatabase","options","image","container","PostgreSqlContainer","withDatabase","withUsername","withPassword","start","connectionString","getConnectionUri","client","postgres","max","onnotice","undefined","db","drizzle","disposed","dispose","end","timeout","catch","stop","migrate","migrationsFolder","migrationsDir","err"],"mappings":";;;;;;;AAoDA,IAAMA,aAAAA,GAAgB,wBAAA;AAOf,IAAMC,kBAAAA,iCACXC,OAAAA,KAAAA;AAEA,EAAA,MAAMC,KAAAA,GAAQD,QAAQC,KAAAA,IAASH,aAAAA;AAE/B,EAAA,MAAMI,SAAAA,GAAwC,MAAM,IAAIC,mBAAAA,CAAoBF,KAAAA,CAAAA,CACzEG,YAAAA,CAAa,gBAAA,CAAA,CACbC,aAAa,WAAA,CAAA,CACbC,YAAAA,CAAa,WAAA,EACbC,KAAAA,EAAK;AAER,EAAA,MAAMC,gBAAAA,GAAmBN,UAAUO,gBAAAA,EAAgB;AACnD,EAAA,MAAMC,MAAAA,GAAcC,SAASH,gBAAAA,EAAkB;IAAEI,GAAAA,EAAK,CAAA;AAAGC,IAAAA,QAAAA,+BAAgBC,MAAAA,EAAN,UAAA;GAAgB,CAAA;AACnF,EAAA,MAAMC,EAAAA,GAAKC,QAAQN,MAAAA,CAAAA;AAEnB,EAAA,IAAIO,QAAAA,GAAW,KAAA;AACf,EAAA,MAAMC,0BAAU,MAAA,CAAA,YAAA;AACd,IAAA,IAAID,QAAAA,EAAU;AACdA,IAAAA,QAAAA,GAAW,IAAA;AACX,IAAA,MAAMP,OAAOS,GAAAA,CAAI;MAAEC,OAAAA,EAAS;KAAE,CAAA,CAAGC,KAAAA,CAAM,MAAMP,MAAAA,CAAAA;AAC7C,IAAA,MAAMZ,UAAUoB,IAAAA,CAAK;MAAEF,OAAAA,EAAS;KAAK,CAAA,CAAGC,KAAAA,CAAM,MAAMP,MAAAA,CAAAA;EACtD,CAAA,EALgB,SAAA,CAAA;AAOhB,EAAA,IAAI;AACF,IAAA,MAAMS,QAAQR,EAAAA,EAAI;AAAES,MAAAA,gBAAAA,EAAkBxB,OAAAA,CAAQyB;KAAc,CAAA;AAC9D,EAAA,CAAA,CAAA,OAASC,GAAAA,EAAK;AACZ,IAAA,MAAMR,OAAAA,EAAAA;AACN,IAAA,MAAMQ,GAAAA;AACR,EAAA;AAEA,EAAA,OAAO;AAAElB,IAAAA,gBAAAA;IAAkBQ,OAAAA,EAASD,EAAAA;AAAIG,IAAAA;AAAQ,GAAA;AAClD,CAAA,EA/BkC,oBAAA","file":"index.js","sourcesContent":["// Public API for `@stackbone/sdk/db/testing`.\n//\n// `createTestDatabase` spins up an ephemeral Postgres container via\n// `@testcontainers/postgresql`, applies a drizzle-kit-shaped migrations folder\n// against it and hands back a ready-to-query Drizzle instance plus a\n// `dispose()` that stops the container. Every call gets its own container, so\n// parallel tests never bleed rows nor share state.\n//\n// Why testcontainers instead of a shared dev DB:\n// - One self-contained primitive: `pnpm test` just works, no\n// `docker compose up` ceremony required of contributors or CI.\n// - True isolation per suite — schemas inside a shared DB still share the\n// server's stats, locks and configuration; containers do not.\n// - Aligns with the rest of the Stackbone monorepo (see\n// `apps/api/src/db/__tests__/setup-postgres.ts`).\n\nimport { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';\nimport { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';\nimport { migrate } from 'drizzle-orm/postgres-js/migrator';\nimport postgres, { type Sql } from 'postgres';\n\n/** Inputs for `createTestDatabase`. */\nexport interface CreateTestDatabaseOptions {\n /**\n * Drizzle-kit-shaped migrations directory: `meta/_journal.json` plus one\n * `<tag>.sql` file per entry. Required — the helper applies the migrations\n * before returning so the first query a test runs already sees the schema.\n */\n migrationsDir: string;\n /**\n * Postgres image to spin up. Defaults to `pgvector/pgvector:pg17` so RAG\n * and AI starters that need the `vector` extension work out of the box.\n * Override with `postgres:17` for a vanilla minimum-footprint container,\n * or with a custom tag (e.g. `stackbone/postgres:local`) when the test\n * suite needs the bundled `pgmq` / `pg_cron` extensions.\n */\n image?: string;\n}\n\n/** Result of `createTestDatabase`. */\nexport interface TestDatabase {\n /** Postgres URL of the running container, ready for sub-processes. */\n connectionString: string;\n /** Drizzle handle bound to the container's database, ready to query. */\n drizzle: PostgresJsDatabase<Record<string, never>>;\n /**\n * Tears the test DB down: closes the pool and stops the container.\n * Idempotent — calling it twice is a no-op.\n */\n dispose: () => Promise<void>;\n}\n\nconst DEFAULT_IMAGE = 'pgvector/pgvector:pg17';\n\n/**\n * Spins up an ephemeral Postgres container, applies the supplied migrations\n * directory against it, and hands back a Drizzle instance + `dispose()` for\n * tear-down. See module header for design rationale.\n */\nexport const createTestDatabase = async (\n options: CreateTestDatabaseOptions,\n): Promise<TestDatabase> => {\n const image = options.image ?? DEFAULT_IMAGE;\n\n const container: StartedPostgreSqlContainer = await new PostgreSqlContainer(image)\n .withDatabase('stackbone_test')\n .withUsername('stackbone')\n .withPassword('stackbone')\n .start();\n\n const connectionString = container.getConnectionUri();\n const client: Sql = postgres(connectionString, { max: 5, onnotice: () => undefined });\n const db = drizzle(client);\n\n let disposed = false;\n const dispose = async (): Promise<void> => {\n if (disposed) return;\n disposed = true;\n await client.end({ timeout: 5 }).catch(() => undefined);\n await container.stop({ timeout: 5000 }).catch(() => undefined);\n };\n\n try {\n await migrate(db, { migrationsFolder: options.migrationsDir });\n } catch (err) {\n await dispose();\n throw err;\n }\n\n return { connectionString, drizzle: db, dispose };\n};\n"]}