@danceroutine/tango-testing 0.1.0

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.
Files changed (62) hide show
  1. package/dist/assertions/assertions.d.ts +7 -0
  2. package/dist/assertions/assertions.js +8 -0
  3. package/dist/assertions/index.d.ts +4 -0
  4. package/dist/assertions/index.js +3 -0
  5. package/dist/assertions-CN6KxXhH.js +15 -0
  6. package/dist/assertions-CN6KxXhH.js.map +1 -0
  7. package/dist/chunk-BkvOhyD0.js +12 -0
  8. package/dist/factories/ModelDataFactory.d.ts +18 -0
  9. package/dist/factories/ModelDataFactory.js +33 -0
  10. package/dist/factories/index.d.ts +4 -0
  11. package/dist/factories/index.js +3 -0
  12. package/dist/factories-CCAZ6E-g.js +40 -0
  13. package/dist/factories-CCAZ6E-g.js.map +1 -0
  14. package/dist/index.d.ts +16 -0
  15. package/dist/index.js +12 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/integration/HarnessStrategyRegistry.d.ts +10 -0
  18. package/dist/integration/TestHarness.d.ts +17 -0
  19. package/dist/integration/config.d.ts +7 -0
  20. package/dist/integration/domain/Dialect.d.ts +4 -0
  21. package/dist/integration/domain/HarnessStrategy.d.ts +17 -0
  22. package/dist/integration/domain/IntegrationHarness.d.ts +21 -0
  23. package/dist/integration/domain/ResetMode.d.ts +5 -0
  24. package/dist/integration/domain/index.d.ts +7 -0
  25. package/dist/integration/index.d.ts +11 -0
  26. package/dist/integration/index.js +3 -0
  27. package/dist/integration/migrations/ApplyAndVerifyMigrations.d.ts +13 -0
  28. package/dist/integration/migrations/AssertMigrationPlan.d.ts +6 -0
  29. package/dist/integration/migrations/IntrospectSchema.d.ts +2 -0
  30. package/dist/integration/migrations/index.d.ts +6 -0
  31. package/dist/integration/orm.d.ts +9 -0
  32. package/dist/integration/orm.js +39 -0
  33. package/dist/integration/strategies/PostgresHarnessStrategy.d.ts +10 -0
  34. package/dist/integration/strategies/PostgresHarnessStrategy.js +95 -0
  35. package/dist/integration/strategies/SqliteHarnessStrategy.d.ts +10 -0
  36. package/dist/integration-CDdpboYz.js +378 -0
  37. package/dist/integration-CDdpboYz.js.map +1 -0
  38. package/dist/mocks/DBClient.d.ts +9 -0
  39. package/dist/mocks/DBClient.js +1 -0
  40. package/dist/mocks/MockQuerySetResult.d.ts +12 -0
  41. package/dist/mocks/MockQuerySetResult.js +1 -0
  42. package/dist/mocks/RepositoryLike.d.ts +12 -0
  43. package/dist/mocks/RepositoryLike.js +1 -0
  44. package/dist/mocks/aMockDBClient.d.ts +2 -0
  45. package/dist/mocks/aMockDBClient.js +13 -0
  46. package/dist/mocks/aMockQuerySet.d.ts +2 -0
  47. package/dist/mocks/aMockQuerySet.js +15 -0
  48. package/dist/mocks/aMockRepository.d.ts +2 -0
  49. package/dist/mocks/aMockRepository.js +20 -0
  50. package/dist/mocks/index.d.ts +9 -0
  51. package/dist/mocks/index.js +6 -0
  52. package/dist/mocks/types.d.ts +33 -0
  53. package/dist/mocks-qo-1vCez.js +72 -0
  54. package/dist/mocks-qo-1vCez.js.map +1 -0
  55. package/dist/version.d.ts +1 -0
  56. package/dist/vitest/index.d.ts +4 -0
  57. package/dist/vitest/index.js +2 -0
  58. package/dist/vitest/registerVitestTango.d.ts +39 -0
  59. package/dist/vitest/registerVitestTango.js +90 -0
  60. package/dist/vitest-PxMJue7R.js +81 -0
  61. package/dist/vitest-PxMJue7R.js.map +1 -0
  62. package/package.json +76 -0
@@ -0,0 +1,7 @@
1
+ import type { GenericModelFactory } from '../factories';
2
+ /**
3
+ * Assertion helpers for Tango models.
4
+ */
5
+ export declare const assertions: {
6
+ matchesSchema<TModel extends GenericModelFactory<Record<string, unknown>, unknown>>(model: TModel, data: unknown): asserts data is ReturnType<TModel["parse"]>;
7
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Assertion helpers for Tango models.
3
+ */
4
+ export const assertions = {
5
+ matchesSchema(model, data) {
6
+ model.parse(data);
7
+ },
8
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Domain boundary barrel: centralizes this subdomain's public contract.
3
+ */
4
+ export { assertions } from './assertions';
@@ -0,0 +1,3 @@
1
+ import { assertions } from "../assertions-CN6KxXhH.js";
2
+
3
+ export { assertions };
@@ -0,0 +1,15 @@
1
+ import { __export } from "./chunk-BkvOhyD0.js";
2
+
3
+ //#region src/assertions/assertions.ts
4
+ const assertions = { matchesSchema(model, data) {
5
+ model.parse(data);
6
+ } };
7
+
8
+ //#endregion
9
+ //#region src/assertions/index.ts
10
+ var assertions_exports = {};
11
+ __export(assertions_exports, { assertions: () => assertions });
12
+
13
+ //#endregion
14
+ export { assertions, assertions_exports };
15
+ //# sourceMappingURL=assertions-CN6KxXhH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertions-CN6KxXhH.js","names":["model: TModel","data: unknown"],"sources":["../src/assertions/assertions.ts","../src/assertions/index.ts"],"sourcesContent":["import type { GenericModelFactory } from '../factories';\n\n/**\n * Assertion helpers for Tango models.\n */\nexport const assertions = {\n matchesSchema<TModel extends GenericModelFactory<any, any>>(\n model: TModel,\n data: unknown\n ): asserts data is ReturnType<TModel['parse']> {\n model.parse(data);\n },\n};\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { assertions } from './assertions';\n"],"mappings":";;;MAKa,aAAa,EACtB,cACIA,OACAC,MAC2C;AAC3C,OAAM,MAAM,KAAK;AACpB,EACJ"}
@@ -0,0 +1,12 @@
1
+
2
+ //#region rolldown:runtime
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ };
10
+
11
+ //#endregion
12
+ export { __export };
@@ -0,0 +1,18 @@
1
+ export type GenericModelFactory<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput = TInput> = {
2
+ create(data: TInput): TOutput;
3
+ parse(data: unknown): TOutput;
4
+ };
5
+ /**
6
+ * Factory class for generating test data with sequences and defaults.
7
+ */
8
+ export declare class ModelDataFactory<TModel extends GenericModelFactory<Record<string, unknown>, unknown>> {
9
+ private model;
10
+ private defaults;
11
+ private sequence;
12
+ constructor(model: TModel, defaults?: Partial<Parameters<TModel['create']>[0]>);
13
+ build(overrides?: Partial<Parameters<TModel['create']>[0]>): ReturnType<TModel['create']>;
14
+ buildList(count: number, overrides?: Partial<Parameters<TModel['create']>[0]>): ReturnType<TModel['create']>[];
15
+ protected sequenceDefaults(): Partial<Parameters<TModel['create']>[0]>;
16
+ resetSequence(): void;
17
+ getSequence(): number;
18
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Factory class for generating test data with sequences and defaults.
3
+ */
4
+ export class ModelDataFactory {
5
+ model;
6
+ defaults;
7
+ sequence = 0;
8
+ constructor(model, defaults = {}) {
9
+ this.model = model;
10
+ this.defaults = defaults;
11
+ }
12
+ build(overrides = {}) {
13
+ this.sequence++;
14
+ const data = {
15
+ ...this.defaults,
16
+ ...this.sequenceDefaults(),
17
+ ...overrides,
18
+ };
19
+ return this.model.create(data);
20
+ }
21
+ buildList(count, overrides = {}) {
22
+ return Array.from({ length: count }, () => this.build(overrides));
23
+ }
24
+ sequenceDefaults() {
25
+ return {};
26
+ }
27
+ resetSequence() {
28
+ this.sequence = 0;
29
+ }
30
+ getSequence() {
31
+ return this.sequence;
32
+ }
33
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Domain boundary barrel: centralizes this subdomain's public contract.
3
+ */
4
+ export { ModelDataFactory, type GenericModelFactory } from './ModelDataFactory';
@@ -0,0 +1,3 @@
1
+ import { ModelDataFactory } from "../factories-CCAZ6E-g.js";
2
+
3
+ export { ModelDataFactory };
@@ -0,0 +1,40 @@
1
+ import { __export } from "./chunk-BkvOhyD0.js";
2
+
3
+ //#region src/factories/ModelDataFactory.ts
4
+ var ModelDataFactory = class {
5
+ sequence = 0;
6
+ constructor(model, defaults = {}) {
7
+ this.model = model;
8
+ this.defaults = defaults;
9
+ }
10
+ build(overrides = {}) {
11
+ this.sequence++;
12
+ const data = {
13
+ ...this.defaults,
14
+ ...this.sequenceDefaults(),
15
+ ...overrides
16
+ };
17
+ return this.model.create(data);
18
+ }
19
+ buildList(count, overrides = {}) {
20
+ return Array.from({ length: count }, () => this.build(overrides));
21
+ }
22
+ sequenceDefaults() {
23
+ return {};
24
+ }
25
+ resetSequence() {
26
+ this.sequence = 0;
27
+ }
28
+ getSequence() {
29
+ return this.sequence;
30
+ }
31
+ };
32
+
33
+ //#endregion
34
+ //#region src/factories/index.ts
35
+ var factories_exports = {};
36
+ __export(factories_exports, { ModelDataFactory: () => ModelDataFactory });
37
+
38
+ //#endregion
39
+ export { ModelDataFactory, factories_exports };
40
+ //# sourceMappingURL=factories-CCAZ6E-g.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factories-CCAZ6E-g.js","names":["model: TModel","defaults: Partial<Parameters<TModel['create']>[0]>","overrides: Partial<Parameters<TModel['create']>[0]>","count: number"],"sources":["../src/factories/ModelDataFactory.ts","../src/factories/index.ts"],"sourcesContent":["export type GenericModelFactory<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput = TInput> = {\n create(data: TInput): TOutput;\n parse(data: unknown): TOutput;\n};\n\n/**\n * Factory class for generating test data with sequences and defaults.\n */\nexport class ModelDataFactory<TModel extends GenericModelFactory<any, any>> {\n private sequence = 0;\n\n constructor(\n private model: TModel,\n private defaults: Partial<Parameters<TModel['create']>[0]> = {}\n ) {}\n\n public build(overrides: Partial<Parameters<TModel['create']>[0]> = {}): ReturnType<TModel['create']> {\n this.sequence++;\n const data = {\n ...this.defaults,\n ...this.sequenceDefaults(),\n ...overrides,\n };\n return this.model.create(data as any);\n }\n\n public buildList(\n count: number,\n overrides: Partial<Parameters<TModel['create']>[0]> = {}\n ): ReturnType<TModel['create']>[] {\n return Array.from({ length: count }, () => this.build(overrides));\n }\n\n protected sequenceDefaults(): Partial<Parameters<TModel['create']>[0]> {\n return {} as any;\n }\n\n public resetSequence(): void {\n this.sequence = 0;\n }\n\n public getSequence(): number {\n return this.sequence;\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { ModelDataFactory, type GenericModelFactory } from './ModelDataFactory';\n"],"mappings":";;;IAQa,mBAAN,MAAqE;CACxE,WAAmB;CAEnB,YACYA,OACAC,WAAqD,CAAE,GACjE;AAAA,OAFU,QAAA;AAAA,OACA,WAAA;CACR;CAEJ,MAAaC,YAAsD,CAAE,GAAgC;AACjG,OAAK;EACL,MAAM,OAAO;GACT,GAAG,KAAK;GACR,GAAG,KAAK,kBAAkB;GAC1B,GAAG;EACN;AACD,SAAO,KAAK,MAAM,OAAO,KAAY;CACxC;CAED,UACIC,OACAD,YAAsD,CAAE,GAC1B;AAC9B,SAAO,MAAM,KAAK,EAAE,QAAQ,MAAO,GAAE,MAAM,KAAK,MAAM,UAAU,CAAC;CACpE;CAED,mBAAuE;AACnE,SAAO,CAAE;CACZ;CAED,gBAA6B;AACzB,OAAK,WAAW;CACnB;CAED,cAA6B;AACzB,SAAO,KAAK;CACf;AACJ"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bundled exports for Django-style domain drill-down imports, plus curated
3
+ * top-level symbols for TS-native ergonomic imports.
4
+ */
5
+ export * as mocks from './mocks/index';
6
+ export * as factories from './factories/index';
7
+ export * as assertionsDomain from './assertions/index';
8
+ export * as integration from './integration/index';
9
+ export * as vitest from './vitest/index';
10
+ export { VERSION as version } from './version';
11
+ export { aMockDBClient, aMockQuerySet, aMockRepository } from './mocks/index';
12
+ export type { DBClient, MockQuerySetResult, RepositoryLike } from './mocks/index';
13
+ export { ModelDataFactory } from './factories/index';
14
+ export type { GenericModelFactory } from './factories/index';
15
+ export { assertions } from './assertions/index';
16
+ export * from './integration/index';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { aMockDBClient, aMockQuerySet, aMockRepository, mocks_exports } from "./mocks-qo-1vCez.js";
2
+ import { ModelDataFactory, factories_exports } from "./factories-CCAZ6E-g.js";
3
+ import { assertions, assertions_exports } from "./assertions-CN6KxXhH.js";
4
+ import { Dialect, HarnessStrategyRegistry, ResetMode, TestHarness, applyAndVerifyMigrations, assertMigrationPlan, createRepositoryFixture, domain_exports, expectQueryResult, integration_exports, introspectSchema, migrations_exports, seedTable } from "./integration-CDdpboYz.js";
5
+ import { vitest_exports } from "./vitest-PxMJue7R.js";
6
+
7
+ //#region src/version.ts
8
+ const VERSION = "0.1.0";
9
+
10
+ //#endregion
11
+ export { Dialect, HarnessStrategyRegistry, ModelDataFactory, ResetMode, TestHarness, aMockDBClient, aMockQuerySet, aMockRepository, applyAndVerifyMigrations, assertMigrationPlan, assertions, assertions_exports as assertionsDomain, createRepositoryFixture, domain_exports as domain, expectQueryResult, factories_exports as factories, integration_exports as integration, introspectSchema, migrations_exports as migrations, mocks_exports as mocks, seedTable, VERSION as version, vitest_exports as vitest };
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["export const VERSION = '0.1.0';\n"],"mappings":";;;;;;;MAAa,UAAU"}
@@ -0,0 +1,10 @@
1
+ import type { Dialect, HarnessStrategy } from './domain';
2
+ export declare class HarnessStrategyRegistry {
3
+ static readonly BRAND: "tango.testing.harness_strategy_registry";
4
+ readonly __tangoBrand: typeof HarnessStrategyRegistry.BRAND;
5
+ private readonly strategies;
6
+ static isHarnessStrategyRegistry(value: unknown): value is HarnessStrategyRegistry;
7
+ register(strategy: HarnessStrategy): this;
8
+ get(dialect: Dialect | string): HarnessStrategy;
9
+ list(): readonly HarnessStrategy[];
10
+ }
@@ -0,0 +1,17 @@
1
+ import { HarnessStrategyRegistry } from './HarnessStrategyRegistry';
2
+ import { Dialect, type HarnessOptions, type HarnessStrategy, type IntegrationHarness } from './domain';
3
+ export declare class TestHarness {
4
+ static readonly BRAND: "tango.testing.test_harness";
5
+ readonly __tangoBrand: typeof TestHarness.BRAND;
6
+ private static defaultRegistry;
7
+ static isTestHarness(value: unknown): value is TestHarness;
8
+ private static ensureRegistry;
9
+ static registerStrategy(strategy: HarnessStrategy): void;
10
+ static getRegistry(): HarnessStrategyRegistry;
11
+ static forDialect(args: {
12
+ dialect: Dialect | string;
13
+ options?: HarnessOptions;
14
+ }, registry?: HarnessStrategyRegistry): Promise<IntegrationHarness>;
15
+ static sqlite(options?: HarnessOptions): Promise<IntegrationHarness>;
16
+ static postgres(options?: HarnessOptions): Promise<IntegrationHarness>;
17
+ }
@@ -0,0 +1,7 @@
1
+ import type { AdapterConfig } from '@danceroutine/tango-orm';
2
+ import { Dialect } from './domain';
3
+ export declare function resolveAdapterConfig(dialect: Dialect, opts: {
4
+ config?: Partial<AdapterConfig>;
5
+ tangoConfigLoader?: () => unknown;
6
+ sqliteFile?: string;
7
+ }): AdapterConfig;
@@ -0,0 +1,4 @@
1
+ export declare enum Dialect {
2
+ Sqlite = "sqlite",
3
+ Postgres = "postgres"
4
+ }
@@ -0,0 +1,17 @@
1
+ import type { Dialect } from './Dialect';
2
+ import type { AdapterConfig } from '@danceroutine/tango-orm';
3
+ import type { DialectTestCapabilities } from './IntegrationHarness';
4
+ import type { IntegrationHarness } from './IntegrationHarness';
5
+ import type { ResetMode } from './ResetMode';
6
+ export interface HarnessOptions {
7
+ config?: Partial<AdapterConfig>;
8
+ tangoConfigLoader?: () => unknown;
9
+ resetMode?: ResetMode;
10
+ schema?: string;
11
+ sqliteFile?: string;
12
+ }
13
+ export interface HarnessStrategy {
14
+ readonly dialect: Dialect | string;
15
+ readonly capabilities: DialectTestCapabilities;
16
+ create(options?: HarnessOptions): Promise<IntegrationHarness>;
17
+ }
@@ -0,0 +1,21 @@
1
+ import type { DBClient } from '@danceroutine/tango-orm';
2
+ import type { MigrationRunner } from '@danceroutine/tango-migrations';
3
+ import type { Dialect } from './Dialect';
4
+ import type { ResetMode } from './ResetMode';
5
+ export interface DialectTestCapabilities {
6
+ transactionalDDL: boolean;
7
+ supportsSchemas: boolean;
8
+ supportsConcurrentIndex: boolean;
9
+ supportsDeferredFkValidation: boolean;
10
+ supportsJsonb: boolean;
11
+ }
12
+ export interface IntegrationHarness {
13
+ readonly dialect: Dialect | string;
14
+ readonly capabilities: DialectTestCapabilities;
15
+ readonly resetMode: ResetMode;
16
+ readonly dbClient: DBClient;
17
+ setup(): Promise<void>;
18
+ reset(): Promise<void>;
19
+ teardown(): Promise<void>;
20
+ migrationRunner(migrationsDir: string): MigrationRunner;
21
+ }
@@ -0,0 +1,5 @@
1
+ export declare enum ResetMode {
2
+ Transaction = "transaction",
3
+ Truncate = "truncate",
4
+ DropSchema = "drop-schema"
5
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Domain boundary barrel: centralizes this subdomain's public contract.
3
+ */
4
+ export { Dialect } from './Dialect';
5
+ export type { HarnessOptions, HarnessStrategy } from './HarnessStrategy';
6
+ export type { DialectTestCapabilities, IntegrationHarness } from './IntegrationHarness';
7
+ export { ResetMode } from './ResetMode';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Domain boundary barrel: exposes namespaced exports for Django-style drill-down
3
+ * imports and curated flat exports for TS-native ergonomics.
4
+ */
5
+ export * as domain from './domain/index';
6
+ export * as migrations from './migrations/index';
7
+ export * from './domain/index';
8
+ export * from './HarnessStrategyRegistry';
9
+ export * from './TestHarness';
10
+ export * from './migrations/index';
11
+ export * from './orm';
@@ -0,0 +1,3 @@
1
+ import { Dialect, HarnessStrategyRegistry, ResetMode, TestHarness, applyAndVerifyMigrations, assertMigrationPlan, createRepositoryFixture, domain_exports, expectQueryResult, introspectSchema, migrations_exports, seedTable } from "../integration-CDdpboYz.js";
2
+
3
+ export { Dialect, HarnessStrategyRegistry, ResetMode, TestHarness, applyAndVerifyMigrations, assertMigrationPlan, createRepositoryFixture, domain_exports as domain, expectQueryResult, introspectSchema, migrations_exports as migrations, seedTable };
@@ -0,0 +1,13 @@
1
+ import type { IntegrationHarness } from '../domain';
2
+ export type ApplyAndVerifyMigrationsOptions = {
3
+ migrationsDir: string;
4
+ toId?: string;
5
+ expectedAppliedIds?: string[];
6
+ };
7
+ export type MigrationStatus = {
8
+ id: string;
9
+ applied: boolean;
10
+ };
11
+ export declare function applyAndVerifyMigrations(harness: IntegrationHarness, options: ApplyAndVerifyMigrationsOptions): Promise<{
12
+ statuses: MigrationStatus[];
13
+ }>;
@@ -0,0 +1,6 @@
1
+ import type { IntegrationHarness } from '../domain';
2
+ export type AssertMigrationPlanOptions = {
3
+ migrationsDir: string;
4
+ expectSqlContains?: string[];
5
+ };
6
+ export declare function assertMigrationPlan(harness: IntegrationHarness, options: AssertMigrationPlanOptions): Promise<string>;
@@ -0,0 +1,2 @@
1
+ import { type IntegrationHarness } from '../domain';
2
+ export declare function introspectSchema(harness: IntegrationHarness): Promise<unknown>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Domain boundary barrel: centralizes this subdomain's public contract.
3
+ */
4
+ export { assertMigrationPlan, type AssertMigrationPlanOptions } from './AssertMigrationPlan';
5
+ export { applyAndVerifyMigrations, type ApplyAndVerifyMigrationsOptions, type MigrationStatus, } from './ApplyAndVerifyMigrations';
6
+ export { introspectSchema } from './IntrospectSchema';
@@ -0,0 +1,9 @@
1
+ import { Repository } from '@danceroutine/tango-orm';
2
+ import type { RepoMeta } from '@danceroutine/tango-orm/query';
3
+ import { type IntegrationHarness } from './domain';
4
+ export declare function seedTable<T extends Record<string, unknown>>(harness: IntegrationHarness, table: string, rows: T[]): Promise<void>;
5
+ export declare function createRepositoryFixture<TModel extends Record<string, unknown>>(input: {
6
+ harness: IntegrationHarness;
7
+ meta: RepoMeta;
8
+ }): Repository<TModel>;
9
+ export declare function expectQueryResult<T>(actual: Promise<T> | T, expected: T): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { Repository } from '@danceroutine/tango-orm';
2
+ import { Dialect } from './domain';
3
+ export async function seedTable(harness, table, rows) {
4
+ if (rows.length === 0) {
5
+ return;
6
+ }
7
+ const columns = Object.keys(rows[0] ?? {});
8
+ if (columns.length === 0) {
9
+ return;
10
+ }
11
+ for (const row of rows) {
12
+ const values = columns.map((column) => {
13
+ const value = row[column];
14
+ if (harness.dialect === Dialect.Sqlite && typeof value === 'boolean') {
15
+ return value ? 1 : 0;
16
+ }
17
+ return value;
18
+ });
19
+ const placeholders = harness.dialect === Dialect.Postgres
20
+ ? columns.map((_, index) => `$${index + 1}`).join(', ')
21
+ : columns.map(() => '?').join(', ');
22
+ await harness.dbClient.query(`INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`, values);
23
+ }
24
+ }
25
+ export function createRepositoryFixture(input) {
26
+ class RepositoryFixture extends Repository {
27
+ meta = input.meta;
28
+ constructor() {
29
+ super(input.harness.dbClient, input.harness.dialect);
30
+ }
31
+ }
32
+ return new RepositoryFixture();
33
+ }
34
+ export async function expectQueryResult(actual, expected) {
35
+ const resolved = await actual;
36
+ if (JSON.stringify(resolved) !== JSON.stringify(expected)) {
37
+ throw new Error(`Expected query result ${JSON.stringify(expected)}, got ${JSON.stringify(resolved)}`);
38
+ }
39
+ }
@@ -0,0 +1,10 @@
1
+ import { Dialect, type DialectTestCapabilities, type HarnessOptions, type HarnessStrategy, type IntegrationHarness } from '../domain';
2
+ export declare class PostgresHarnessStrategy implements HarnessStrategy {
3
+ static readonly BRAND: "tango.testing.postgres_harness_strategy";
4
+ readonly __tangoBrand: typeof PostgresHarnessStrategy.BRAND;
5
+ readonly dialect: Dialect;
6
+ readonly capabilities: DialectTestCapabilities;
7
+ static isPostgresHarnessStrategy(value: unknown): value is PostgresHarnessStrategy;
8
+ private static buildSchemaName;
9
+ create(options?: HarnessOptions): Promise<IntegrationHarness>;
10
+ }
@@ -0,0 +1,95 @@
1
+ import { MigrationRunner } from '@danceroutine/tango-migrations';
2
+ import { PostgresAdapter } from '@danceroutine/tango-orm/connection';
3
+ import { resolveAdapterConfig } from '../config';
4
+ import { Dialect, ResetMode, } from '../domain';
5
+ export class PostgresHarnessStrategy {
6
+ static BRAND = 'tango.testing.postgres_harness_strategy';
7
+ __tangoBrand = PostgresHarnessStrategy.BRAND;
8
+ dialect = Dialect.Postgres;
9
+ capabilities = {
10
+ transactionalDDL: true,
11
+ supportsSchemas: true,
12
+ supportsConcurrentIndex: true,
13
+ supportsDeferredFkValidation: true,
14
+ supportsJsonb: true,
15
+ };
16
+ static isPostgresHarnessStrategy(value) {
17
+ return (typeof value === 'object' &&
18
+ value !== null &&
19
+ value.__tangoBrand === PostgresHarnessStrategy.BRAND);
20
+ }
21
+ static buildSchemaName(explicitSchema) {
22
+ if (explicitSchema)
23
+ return explicitSchema;
24
+ const random = Math.random().toString(36).slice(2, 8);
25
+ return `tango_test_${Date.now()}_${random}`;
26
+ }
27
+ async create(options = {}) {
28
+ const config = resolveAdapterConfig(Dialect.Postgres, {
29
+ config: options.config,
30
+ tangoConfigLoader: options.tangoConfigLoader,
31
+ });
32
+ const adapter = new PostgresAdapter();
33
+ const schemaName = PostgresHarnessStrategy.buildSchemaName(options.schema);
34
+ const resetMode = options.resetMode ?? ResetMode.DropSchema;
35
+ let client = null;
36
+ const ensureSearchPath = async () => {
37
+ const dbClient = client;
38
+ await dbClient.query(`CREATE SCHEMA IF NOT EXISTS \"${schemaName}\"`);
39
+ await dbClient.query(`SET search_path TO \"${schemaName}\"`);
40
+ };
41
+ const recreateSchema = async () => {
42
+ const dbClient = client;
43
+ await dbClient.query(`DROP SCHEMA IF EXISTS \"${schemaName}\" CASCADE`);
44
+ await dbClient.query(`CREATE SCHEMA \"${schemaName}\"`);
45
+ await dbClient.query(`SET search_path TO \"${schemaName}\"`);
46
+ };
47
+ const harness = {
48
+ dialect: Dialect.Postgres,
49
+ capabilities: this.capabilities,
50
+ resetMode,
51
+ get dbClient() {
52
+ if (!client) {
53
+ throw new Error('Postgres harness not initialized. Call setup() first.');
54
+ }
55
+ return client;
56
+ },
57
+ async setup() {
58
+ client = await adapter.connect(config);
59
+ await ensureSearchPath();
60
+ },
61
+ async reset() {
62
+ if (!client) {
63
+ throw new Error('Postgres harness not initialized. Call setup() first.');
64
+ }
65
+ if (resetMode === ResetMode.DropSchema || resetMode === ResetMode.Transaction) {
66
+ await recreateSchema();
67
+ return;
68
+ }
69
+ const { rows } = await client.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'`, [schemaName]);
70
+ for (const row of rows) {
71
+ await client.query(`TRUNCATE TABLE \"${schemaName}\".\"${row.table_name}\" RESTART IDENTITY CASCADE`);
72
+ }
73
+ await client.query(`SET search_path TO \"${schemaName}\"`);
74
+ },
75
+ async teardown() {
76
+ if (!client)
77
+ return;
78
+ try {
79
+ await client.query(`DROP SCHEMA IF EXISTS \"${schemaName}\" CASCADE`);
80
+ }
81
+ finally {
82
+ await client.close();
83
+ client = null;
84
+ }
85
+ },
86
+ migrationRunner(migrationsDir) {
87
+ if (!client) {
88
+ throw new Error('Postgres harness not initialized. Call setup() first.');
89
+ }
90
+ return new MigrationRunner(client, 'postgres', migrationsDir);
91
+ },
92
+ };
93
+ return harness;
94
+ }
95
+ }
@@ -0,0 +1,10 @@
1
+ import { Dialect, type DialectTestCapabilities, type HarnessOptions, type HarnessStrategy, type IntegrationHarness } from '../domain';
2
+ export declare class SqliteHarnessStrategy implements HarnessStrategy {
3
+ static readonly BRAND: "tango.testing.sqlite_harness_strategy";
4
+ readonly __tangoBrand: typeof SqliteHarnessStrategy.BRAND;
5
+ readonly dialect: Dialect;
6
+ readonly capabilities: DialectTestCapabilities;
7
+ static isSqliteHarnessStrategy(value: unknown): value is SqliteHarnessStrategy;
8
+ private static dropAllTables;
9
+ create(options?: HarnessOptions): Promise<IntegrationHarness>;
10
+ }