@fragno-dev/db 0.2.1 → 0.2.2

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 (60) hide show
  1. package/.turbo/turbo-build.log +34 -30
  2. package/CHANGELOG.md +32 -0
  3. package/dist/adapters/generic-sql/query/where-builder.js +1 -1
  4. package/dist/db-fragment-definition-builder.d.ts +27 -89
  5. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  6. package/dist/db-fragment-definition-builder.js +16 -56
  7. package/dist/db-fragment-definition-builder.js.map +1 -1
  8. package/dist/fragments/internal-fragment.d.ts +94 -8
  9. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  10. package/dist/fragments/internal-fragment.js +56 -55
  11. package/dist/fragments/internal-fragment.js.map +1 -1
  12. package/dist/hooks/hooks.d.ts +5 -3
  13. package/dist/hooks/hooks.d.ts.map +1 -1
  14. package/dist/hooks/hooks.js +38 -37
  15. package/dist/hooks/hooks.js.map +1 -1
  16. package/dist/mod.d.ts +3 -3
  17. package/dist/mod.d.ts.map +1 -1
  18. package/dist/mod.js +4 -4
  19. package/dist/mod.js.map +1 -1
  20. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +351 -100
  21. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  22. package/dist/query/unit-of-work/execute-unit-of-work.js +431 -263
  23. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  24. package/dist/query/unit-of-work/unit-of-work.d.ts +17 -8
  25. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  26. package/dist/query/unit-of-work/unit-of-work.js +24 -8
  27. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  28. package/dist/query/value-decoding.js +1 -1
  29. package/dist/schema/create.d.ts +3 -1
  30. package/dist/schema/create.d.ts.map +1 -1
  31. package/dist/schema/create.js +2 -1
  32. package/dist/schema/create.js.map +1 -1
  33. package/dist/schema/generate-id.d.ts +20 -0
  34. package/dist/schema/generate-id.d.ts.map +1 -0
  35. package/dist/schema/generate-id.js +28 -0
  36. package/dist/schema/generate-id.js.map +1 -0
  37. package/package.json +1 -1
  38. package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
  39. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
  40. package/src/db-fragment-definition-builder.test.ts +58 -42
  41. package/src/db-fragment-definition-builder.ts +58 -248
  42. package/src/db-fragment-instantiator.test.ts +64 -88
  43. package/src/db-fragment-integration.test.ts +292 -142
  44. package/src/fragments/internal-fragment.test.ts +272 -266
  45. package/src/fragments/internal-fragment.ts +155 -121
  46. package/src/hooks/hooks.test.ts +248 -256
  47. package/src/hooks/hooks.ts +74 -63
  48. package/src/mod.ts +14 -4
  49. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1494 -1464
  50. package/src/query/unit-of-work/execute-unit-of-work.ts +1685 -590
  51. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  52. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +20 -20
  53. package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
  54. package/src/query/unit-of-work/unit-of-work.ts +26 -13
  55. package/src/schema/create.ts +2 -0
  56. package/src/schema/generate-id.test.ts +57 -0
  57. package/src/schema/generate-id.ts +38 -0
  58. package/src/shared/config.ts +0 -10
  59. package/src/shared/connection-pool.ts +0 -24
  60. package/src/shared/prisma.ts +0 -45
@@ -722,51 +722,51 @@ describe("UOW Coordinator - Parent-Child Execution", () => {
722
722
  // If we got here without Node.js throwing an unhandled rejection, the test passes
723
723
  });
724
724
 
725
- it("should inherit nonce from parent to children for idempotent operations", () => {
725
+ it("should inherit idempotencyKey from parent to children for idempotent operations", () => {
726
726
  const executor = createMockExecutor();
727
727
  const parentUow = createUnitOfWork(createCompiler(), executor, createMockDecoder());
728
728
 
729
- // Parent UOW should have a nonce
730
- const parentNonce = parentUow.nonce;
731
- expect(parentNonce).toBeDefined();
732
- expect(typeof parentNonce).toBe("string");
733
- expect(parentNonce.length).toBeGreaterThan(0);
729
+ // Parent UOW should have an idempotencyKey
730
+ const parentIdempotencyKey = parentUow.idempotencyKey;
731
+ expect(parentIdempotencyKey).toBeDefined();
732
+ expect(typeof parentIdempotencyKey).toBe("string");
733
+ expect(parentIdempotencyKey.length).toBeGreaterThan(0);
734
734
 
735
735
  // Create first child
736
736
  const child1 = parentUow.restrict();
737
- expect(child1.nonce).toBe(parentNonce);
737
+ expect(child1.idempotencyKey).toBe(parentIdempotencyKey);
738
738
 
739
739
  // Create second child (sibling to child1)
740
740
  const child2 = parentUow.restrict();
741
- expect(child2.nonce).toBe(parentNonce);
741
+ expect(child2.idempotencyKey).toBe(parentIdempotencyKey);
742
742
 
743
743
  // Create nested child (child of child1)
744
744
  const grandchild = child1.restrict();
745
- expect(grandchild.nonce).toBe(parentNonce);
745
+ expect(grandchild.idempotencyKey).toBe(parentIdempotencyKey);
746
746
 
747
- // All UOWs in the hierarchy should share the same nonce
748
- expect(parentUow.nonce).toBe(child1.nonce);
749
- expect(child1.nonce).toBe(child2.nonce);
750
- expect(child2.nonce).toBe(grandchild.nonce);
747
+ // All UOWs in the hierarchy should share the same idempotencyKey
748
+ expect(parentUow.idempotencyKey).toBe(child1.idempotencyKey);
749
+ expect(child1.idempotencyKey).toBe(child2.idempotencyKey);
750
+ expect(child2.idempotencyKey).toBe(grandchild.idempotencyKey);
751
751
  });
752
752
 
753
- it("should generate different nonces for separate UOW hierarchies", () => {
753
+ it("should generate different idempotencyKeys for separate UOW hierarchies", () => {
754
754
  const executor = createMockExecutor();
755
755
 
756
756
  // Create two separate parent UOWs
757
757
  const parentUow1 = createUnitOfWork(createCompiler(), executor, createMockDecoder());
758
758
  const parentUow2 = createUnitOfWork(createCompiler(), executor, createMockDecoder());
759
759
 
760
- // They should have different nonces
761
- expect(parentUow1.nonce).not.toBe(parentUow2.nonce);
760
+ // They should have different idempotencyKeys
761
+ expect(parentUow1.idempotencyKey).not.toBe(parentUow2.idempotencyKey);
762
762
 
763
- // But children within each hierarchy should inherit their parent's nonce
763
+ // But children within each hierarchy should inherit their parent's idempotencyKey
764
764
  const child1 = parentUow1.restrict();
765
765
  const child2 = parentUow2.restrict();
766
766
 
767
- expect(child1.nonce).toBe(parentUow1.nonce);
768
- expect(child2.nonce).toBe(parentUow2.nonce);
769
- expect(child1.nonce).not.toBe(child2.nonce);
767
+ expect(child1.idempotencyKey).toBe(parentUow1.idempotencyKey);
768
+ expect(child2.idempotencyKey).toBe(parentUow2.idempotencyKey);
769
+ expect(child1.idempotencyKey).not.toBe(child2.idempotencyKey);
770
770
  });
771
771
 
772
772
  it("should not cause unhandled rejection when service method awaits retrievalPhase and executeRetrieve fails", async () => {
@@ -838,6 +838,70 @@ describe("DeleteBuilder with string ID", () => {
838
838
  });
839
839
  });
840
840
 
841
+ describe("generateId", () => {
842
+ const testSchema = schema((s) =>
843
+ s.addTable("users", (t) =>
844
+ t.addColumn("id", idColumn()).addColumn("email", "string").addColumn("name", "string"),
845
+ ),
846
+ );
847
+
848
+ it("should generate a new FragnoId without creating a record", () => {
849
+ const uow = createUnitOfWork(createMockCompiler(), createMockExecutor(), createMockDecoder());
850
+
851
+ const id = uow.forSchema(testSchema).generateId("users");
852
+
853
+ expect(id).toBeInstanceOf(FragnoId);
854
+ expect(id.externalId).toBeDefined();
855
+ expect(typeof id.externalId).toBe("string");
856
+ expect(id.externalId.length).toBeGreaterThan(0);
857
+ expect(id.version).toBe(0);
858
+
859
+ // No mutation operations should be added
860
+ expect(uow.getMutationOperations()).toHaveLength(0);
861
+ });
862
+
863
+ it("should generate unique IDs on each call", () => {
864
+ const uow = createUnitOfWork(createMockCompiler(), createMockExecutor(), createMockDecoder());
865
+ const typedUow = uow.forSchema(testSchema);
866
+
867
+ const id1 = typedUow.generateId("users");
868
+ const id2 = typedUow.generateId("users");
869
+
870
+ expect(id1.externalId).not.toBe(id2.externalId);
871
+ });
872
+
873
+ it("should allow using generated ID in create", async () => {
874
+ const executor = {
875
+ executeRetrievalPhase: async () => [],
876
+ executeMutationPhase: async () => ({
877
+ success: true,
878
+ createdInternalIds: [1n],
879
+ }),
880
+ };
881
+
882
+ const uow = createUnitOfWork(createMockCompiler(), executor, createMockDecoder());
883
+ const typedUow = uow.forSchema(testSchema);
884
+
885
+ const id = typedUow.generateId("users");
886
+ typedUow.create("users", { id, email: "test@example.com", name: "Test" });
887
+
888
+ await uow.executeMutations();
889
+ const createdIds = uow.getCreatedIds();
890
+
891
+ expect(createdIds).toHaveLength(1);
892
+ expect(createdIds[0].externalId).toBe(id.externalId);
893
+ });
894
+
895
+ it("should throw for non-existent table", () => {
896
+ const uow = createUnitOfWork(createMockCompiler(), createMockExecutor(), createMockDecoder());
897
+
898
+ expect(() => {
899
+ // @ts-expect-error - testing runtime error for non-existent table
900
+ uow.forSchema(testSchema).generateId("nonexistent");
901
+ }).toThrow("Table nonexistent not found in schema");
902
+ });
903
+ });
904
+
841
905
  describe("getCreatedIds", () => {
842
906
  const testSchema = schema((s) =>
843
907
  s.addTable("users", (t) =>
@@ -7,6 +7,7 @@ import type {
7
7
  Relation,
8
8
  } from "../../schema/create";
9
9
  import { FragnoId } from "../../schema/create";
10
+ import { generateId } from "../../schema/generate-id";
10
11
  import type { Condition, ConditionBuilder } from "../condition-builder";
11
12
  import type {
12
13
  SelectClause,
@@ -262,13 +263,9 @@ export type MutationResult =
262
263
  * Executor interface for Unit of Work operations
263
264
  */
264
265
  export interface UOWExecutor<TOutput, TRawResult = unknown> {
265
- /**
266
- * Execute the retrieval phase - all queries run in a single transaction for snapshot isolation
267
- */
268
266
  executeRetrievalPhase(retrievalBatch: TOutput[]): Promise<TRawResult[]>;
269
267
 
270
268
  /**
271
- * Execute the mutation phase - all queries run in a transaction with version checks
272
269
  * Returns success status indicating if mutations completed without conflicts,
273
270
  * and internal IDs for create operations (null if database doesn't support RETURNING)
274
271
  */
@@ -903,7 +900,7 @@ export interface IUnitOfWork {
903
900
  // Getters (schema-agnostic)
904
901
  readonly state: UOWState;
905
902
  readonly name: string | undefined;
906
- readonly nonce: string;
903
+ readonly idempotencyKey: string;
907
904
  readonly retrievalPhase: Promise<unknown[]>;
908
905
  readonly mutationPhase: Promise<void>;
909
906
 
@@ -967,7 +964,7 @@ export function createUnitOfWork(
967
964
  export interface UnitOfWorkConfig {
968
965
  dryRun?: boolean;
969
966
  onQuery?: (query: unknown) => void;
970
- nonce?: string;
967
+ idempotencyKey?: string;
971
968
  }
972
969
 
973
970
  /**
@@ -1199,7 +1196,7 @@ class UOWChildCoordinator<TRawInput> {
1199
1196
  export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1200
1197
  #name?: string;
1201
1198
  #config?: UnitOfWorkConfig;
1202
- #nonce: string;
1199
+ #idempotencyKey: string;
1203
1200
 
1204
1201
  #state: UOWState = "building-retrieval";
1205
1202
 
@@ -1243,7 +1240,7 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1243
1240
  this.#schemaNamespaceMap = schemaNamespaceMap ?? new WeakMap();
1244
1241
  this.#name = name;
1245
1242
  this.#config = config;
1246
- this.#nonce = config?.nonce ?? crypto.randomUUID();
1243
+ this.#idempotencyKey = config?.idempotencyKey ?? crypto.randomUUID();
1247
1244
  }
1248
1245
 
1249
1246
  /**
@@ -1293,7 +1290,7 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1293
1290
  this.#executor,
1294
1291
  this.#decoder,
1295
1292
  this.#name,
1296
- { ...this.#config, nonce: this.#nonce },
1293
+ { ...this.#config, idempotencyKey: this.#idempotencyKey },
1297
1294
  this.#schemaNamespaceMap,
1298
1295
  );
1299
1296
  child.#coordinator.setAsRestricted(this, this.#coordinator);
@@ -1394,8 +1391,8 @@ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
1394
1391
  return this.#name;
1395
1392
  }
1396
1393
 
1397
- get nonce(): string {
1398
- return this.#nonce;
1394
+ get idempotencyKey(): string {
1395
+ return this.#idempotencyKey;
1399
1396
  }
1400
1397
 
1401
1398
  /**
@@ -1674,8 +1671,8 @@ export class TypedUnitOfWork<
1674
1671
  return this.#uow.name;
1675
1672
  }
1676
1673
 
1677
- get nonce(): string {
1678
- return this.#uow.nonce;
1674
+ get idempotencyKey(): string {
1675
+ return this.#uow.idempotencyKey;
1679
1676
  }
1680
1677
 
1681
1678
  get state() {
@@ -1962,6 +1959,22 @@ export class TypedUnitOfWork<
1962
1959
  return this as any;
1963
1960
  }
1964
1961
 
1962
+ /**
1963
+ * Generate a new ID for a table without creating a record.
1964
+ * This is useful when you need to reference an ID before actually creating the record,
1965
+ * or when you need to pass the ID to external services.
1966
+ *
1967
+ * @example
1968
+ * ```ts
1969
+ * const userId = uow.generateId("users");
1970
+ * // Use userId in related records or pass to external services
1971
+ * uow.create("users", { id: userId, name: "John" });
1972
+ * ```
1973
+ */
1974
+ generateId<TableName extends keyof TSchema["tables"] & string>(tableName: TableName): FragnoId {
1975
+ return generateId(this.#schema, tableName);
1976
+ }
1977
+
1965
1978
  create<TableName extends keyof TSchema["tables"] & string>(
1966
1979
  tableName: TableName,
1967
1980
  values: TableToInsertValues<TSchema["tables"][TableName]>,
@@ -1,5 +1,7 @@
1
1
  import { createId } from "../id";
2
2
 
3
+ export { generateId } from "./generate-id";
4
+
3
5
  export type AnySchema = Schema<Record<string, AnyTable>>;
4
6
 
5
7
  export type AnyRelation = Relation;
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { schema, idColumn, FragnoId } from "./create";
3
+ import { generateId } from "./generate-id";
4
+
5
+ describe("generateId", () => {
6
+ const testSchema = schema((s) =>
7
+ s.addTable("users", (t) =>
8
+ t.addColumn("id", idColumn()).addColumn("email", "string").addColumn("name", "string"),
9
+ ),
10
+ );
11
+
12
+ it("should generate a new FragnoId", () => {
13
+ const id = generateId(testSchema, "users");
14
+
15
+ expect(id).toBeInstanceOf(FragnoId);
16
+ expect(id.externalId).toBeDefined();
17
+ expect(typeof id.externalId).toBe("string");
18
+ expect(id.externalId.length).toBeGreaterThan(0);
19
+ expect(id.version).toBe(0);
20
+ });
21
+
22
+ it("should generate unique IDs on each call", () => {
23
+ const id1 = generateId(testSchema, "users");
24
+ const id2 = generateId(testSchema, "users");
25
+
26
+ expect(id1.externalId).not.toBe(id2.externalId);
27
+ });
28
+
29
+ it("should throw for non-existent table", () => {
30
+ expect(() => {
31
+ // @ts-expect-error - testing runtime error for non-existent table
32
+ generateId(testSchema, "nonexistent");
33
+ }).toThrow("Table nonexistent not found in schema");
34
+ });
35
+
36
+ it("should work with multiple tables", () => {
37
+ const multiTableSchema = schema((s) =>
38
+ s
39
+ .addTable("users", (t) =>
40
+ t.addColumn("id", idColumn()).addColumn("email", "string").addColumn("name", "string"),
41
+ )
42
+ .addTable("posts", (t) =>
43
+ t
44
+ .addColumn("id", idColumn())
45
+ .addColumn("title", "string")
46
+ .addColumn("authorId", "string"),
47
+ ),
48
+ );
49
+
50
+ const userId = generateId(multiTableSchema, "users");
51
+ const postId = generateId(multiTableSchema, "posts");
52
+
53
+ expect(userId).toBeInstanceOf(FragnoId);
54
+ expect(postId).toBeInstanceOf(FragnoId);
55
+ expect(userId.externalId).not.toBe(postId.externalId);
56
+ });
57
+ });
@@ -0,0 +1,38 @@
1
+ import type { AnySchema } from "./create";
2
+ import { FragnoId } from "./create";
3
+
4
+ /**
5
+ * Generate a new ID for a table without creating a record.
6
+ * This is useful when you need to reference an ID before actually creating the record,
7
+ * or when you need to pass the ID to external services.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const userId = generateId(mySchema, "users");
12
+ * // Use userId in related records or pass to external services
13
+ * uow.create("users", { id: userId, name: "John" });
14
+ * ```
15
+ */
16
+ export function generateId<
17
+ TSchema extends AnySchema,
18
+ TableName extends keyof TSchema["tables"] & string,
19
+ >(schema: TSchema, tableName: TableName): FragnoId {
20
+ const tableSchema = schema.tables[tableName];
21
+ if (!tableSchema) {
22
+ throw new Error(`Table ${tableName} not found in schema`);
23
+ }
24
+
25
+ const idColumn = tableSchema.getIdColumn();
26
+ const generated = idColumn.generateDefaultValue();
27
+ if (generated === undefined) {
28
+ throw new Error(`ID column ${idColumn.ormName} on table ${tableName} has no default generator`);
29
+ }
30
+
31
+ if (typeof generated !== "string") {
32
+ throw new Error(
33
+ `ID column ${idColumn.ormName} on table ${tableName} has no default generator that generates a string.`,
34
+ );
35
+ }
36
+
37
+ return FragnoId.fromExternal(generated, 0);
38
+ }
@@ -1,10 +0,0 @@
1
- import type { AnySchema } from "../schema/create";
2
-
3
- export interface LibraryConfig<TSchemas extends AnySchema[] = AnySchema[]> {
4
- namespace: string;
5
-
6
- /**
7
- * different versions of schemas (sorted in ascending order)
8
- */
9
- schemas: TSchemas;
10
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Represents an active database connection that can be released back to the pool.
3
- */
4
- export interface Connection<TDatabase> {
5
- db: TDatabase;
6
- release(): Promise<void>;
7
- }
8
-
9
- /**
10
- * Connection pool interface for managing database connections.
11
- * Adapter-specific implementations should be used (e.g., createKyselyConnectionPool, createDrizzleConnectionPool).
12
- */
13
- export interface ConnectionPool<TDatabase> {
14
- /** Acquire a connection from the pool */
15
- connect(): Promise<Connection<TDatabase>>;
16
- /**
17
- * Get the database instance synchronously. Only works if the pool has already been initialized
18
- * via connect().
19
- * @throws an error if called before the pool is initialized.
20
- */
21
- getDatabaseSync(): TDatabase;
22
- /** Close the pool and cleanup resources */
23
- close(): Promise<void>;
24
- }
@@ -1,45 +0,0 @@
1
- export type PrismaClient = Record<
2
- string,
3
- {
4
- count: (options: {
5
- select: Record<string, unknown>;
6
- where?: object;
7
- }) => Promise<Record<string, number>>;
8
- upsert: (options: {
9
- where: object;
10
- update: Record<string, unknown>;
11
- create: Record<string, unknown>;
12
- }) => Promise<void>;
13
-
14
- create: (options: { data: Record<string, unknown> }) => Promise<Record<string, unknown>>;
15
-
16
- createMany: (options: { data: Record<string, unknown>[] }) => Promise<void>;
17
-
18
- delete: (options: { where: object }) => Promise<Record<string, unknown>>;
19
-
20
- deleteMany: (options: { where?: object }) => Promise<void>;
21
-
22
- findFirst: (options: {
23
- where: object;
24
- select?: Record<string, unknown>;
25
- orderBy?: OrderBy | OrderBy[];
26
- skip?: number;
27
- }) => Promise<Record<string, unknown> | null>;
28
-
29
- findMany: (options: {
30
- where?: object;
31
- select?: Record<string, unknown>;
32
- orderBy?: OrderBy | OrderBy[];
33
- skip?: number;
34
- take?: number;
35
- }) => Promise<Record<string, unknown>[]>;
36
-
37
- updateMany: (options: { where?: object; data: Record<string, unknown> }) => Promise<void>;
38
- }
39
- > & {
40
- $transaction: <T>(v: (tx: PrismaClient) => T | Promise<T>) => Promise<T>;
41
- };
42
-
43
- export type OrderBy = {
44
- [k: string]: "asc" | "desc" | OrderBy;
45
- };