@fragno-dev/db 0.0.1 → 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 (200) hide show
  1. package/.turbo/turbo-build.log +137 -13
  2. package/.turbo/turbo-test.log +36 -0
  3. package/CHANGELOG.md +7 -0
  4. package/dist/adapters/adapters.d.ts +18 -0
  5. package/dist/adapters/adapters.d.ts.map +1 -0
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +21 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -0
  8. package/dist/adapters/drizzle/drizzle-adapter.js +62 -0
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -0
  10. package/dist/adapters/drizzle/drizzle-query.d.ts +17 -0
  11. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -0
  12. package/dist/adapters/drizzle/drizzle-query.js +139 -0
  13. package/dist/adapters/drizzle/drizzle-query.js.map +1 -0
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +9 -0
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -0
  16. package/dist/adapters/drizzle/drizzle-uow-compiler.js +300 -0
  17. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -0
  18. package/dist/adapters/drizzle/drizzle-uow-decoder.js +82 -0
  19. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -0
  20. package/dist/adapters/drizzle/drizzle-uow-executor.js +125 -0
  21. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +1 -0
  22. package/dist/adapters/drizzle/generate.js +273 -0
  23. package/dist/adapters/drizzle/generate.js.map +1 -0
  24. package/dist/adapters/drizzle/join-column-utils.js +28 -0
  25. package/dist/adapters/drizzle/join-column-utils.js.map +1 -0
  26. package/dist/adapters/drizzle/shared.js +11 -0
  27. package/dist/adapters/drizzle/shared.js.map +1 -0
  28. package/dist/adapters/kysely/kysely-adapter.d.ts +23 -0
  29. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -0
  30. package/dist/adapters/kysely/kysely-adapter.js +119 -0
  31. package/dist/adapters/kysely/kysely-adapter.js.map +1 -0
  32. package/dist/adapters/kysely/kysely-query-builder.js +306 -0
  33. package/dist/adapters/kysely/kysely-query-builder.js.map +1 -0
  34. package/dist/adapters/kysely/kysely-query-compiler.js +67 -0
  35. package/dist/adapters/kysely/kysely-query-compiler.js.map +1 -0
  36. package/dist/adapters/kysely/kysely-query.js +158 -0
  37. package/dist/adapters/kysely/kysely-query.js.map +1 -0
  38. package/dist/adapters/kysely/kysely-uow-compiler.js +139 -0
  39. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -0
  40. package/dist/adapters/kysely/kysely-uow-executor.js +89 -0
  41. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -0
  42. package/dist/adapters/kysely/migration/execute.js +176 -0
  43. package/dist/adapters/kysely/migration/execute.js.map +1 -0
  44. package/dist/fragment.d.ts +54 -0
  45. package/dist/fragment.d.ts.map +1 -0
  46. package/dist/fragment.js +92 -0
  47. package/dist/fragment.js.map +1 -0
  48. package/dist/id.d.ts +2 -0
  49. package/dist/migration-engine/auto-from-schema.js +116 -0
  50. package/dist/migration-engine/auto-from-schema.js.map +1 -0
  51. package/dist/migration-engine/create.d.ts +41 -0
  52. package/dist/migration-engine/create.d.ts.map +1 -0
  53. package/dist/migration-engine/create.js +58 -0
  54. package/dist/migration-engine/create.js.map +1 -0
  55. package/dist/migration-engine/shared.d.ts +90 -0
  56. package/dist/migration-engine/shared.d.ts.map +1 -0
  57. package/dist/migration-engine/shared.js +8 -0
  58. package/dist/migration-engine/shared.js.map +1 -0
  59. package/dist/mod.d.ts +55 -2
  60. package/dist/mod.d.ts.map +1 -1
  61. package/dist/mod.js +111 -2
  62. package/dist/mod.js.map +1 -1
  63. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js +108 -0
  64. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column-builder.js.map +1 -0
  65. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js +55 -0
  66. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/column.js.map +1 -0
  67. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js +18 -0
  68. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/entity.js.map +1 -0
  69. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js +183 -0
  70. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/common.js.map +1 -0
  71. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js +58 -0
  72. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/columns/enum.js.map +1 -0
  73. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js +68 -0
  74. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/foreign-keys.js.map +1 -0
  75. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js +56 -0
  76. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/unique-constraint.js.map +1 -0
  77. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js +65 -0
  78. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/pg-core/utils/array.js.map +1 -0
  79. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js +81 -0
  80. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/conditions.js.map +1 -0
  81. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js +13 -0
  82. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/expressions/select.js.map +1 -0
  83. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js +10 -0
  84. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/functions/aggregate.js.map +1 -0
  85. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js +372 -0
  86. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/sql/sql.js.map +1 -0
  87. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js +23 -0
  88. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/subquery.js.map +1 -0
  89. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js +62 -0
  90. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.js.map +1 -0
  91. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js +6 -0
  92. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/table.utils.js.map +1 -0
  93. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js +8 -0
  94. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing-utils.js.map +1 -0
  95. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js +8 -0
  96. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/tracing.js.map +1 -0
  97. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js +6 -0
  98. package/dist/node_modules/.bun/drizzle-orm@0.44.6_4fae081eecb963e2/node_modules/drizzle-orm/view-common.js.map +1 -0
  99. package/dist/query/condition-builder.d.ts +41 -0
  100. package/dist/query/condition-builder.d.ts.map +1 -0
  101. package/dist/query/condition-builder.js +93 -0
  102. package/dist/query/condition-builder.js.map +1 -0
  103. package/dist/query/cursor.d.ts +88 -0
  104. package/dist/query/cursor.d.ts.map +1 -0
  105. package/dist/query/cursor.js +103 -0
  106. package/dist/query/cursor.js.map +1 -0
  107. package/dist/query/orm/orm.d.ts +18 -0
  108. package/dist/query/orm/orm.d.ts.map +1 -0
  109. package/dist/query/orm/orm.js +48 -0
  110. package/dist/query/orm/orm.js.map +1 -0
  111. package/dist/query/query.d.ts +79 -0
  112. package/dist/query/query.d.ts.map +1 -0
  113. package/dist/query/query.js +1 -0
  114. package/dist/query/result-transform.js +155 -0
  115. package/dist/query/result-transform.js.map +1 -0
  116. package/dist/query/unit-of-work.d.ts +435 -0
  117. package/dist/query/unit-of-work.d.ts.map +1 -0
  118. package/dist/query/unit-of-work.js +549 -0
  119. package/dist/query/unit-of-work.js.map +1 -0
  120. package/dist/schema/create.d.ts +273 -116
  121. package/dist/schema/create.d.ts.map +1 -1
  122. package/dist/schema/create.js +410 -222
  123. package/dist/schema/create.js.map +1 -1
  124. package/dist/schema/serialize.js +101 -0
  125. package/dist/schema/serialize.js.map +1 -0
  126. package/dist/schema-generator/schema-generator.d.ts +15 -0
  127. package/dist/schema-generator/schema-generator.d.ts.map +1 -0
  128. package/dist/shared/providers.d.ts +6 -0
  129. package/dist/shared/providers.d.ts.map +1 -0
  130. package/dist/util/import-generator.js +26 -0
  131. package/dist/util/import-generator.js.map +1 -0
  132. package/dist/util/parse.js +15 -0
  133. package/dist/util/parse.js.map +1 -0
  134. package/dist/util/types.d.ts +8 -0
  135. package/dist/util/types.d.ts.map +1 -0
  136. package/package.json +63 -2
  137. package/src/adapters/adapters.ts +22 -0
  138. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +433 -0
  139. package/src/adapters/drizzle/drizzle-adapter.test.ts +122 -0
  140. package/src/adapters/drizzle/drizzle-adapter.ts +118 -0
  141. package/src/adapters/drizzle/drizzle-query.ts +234 -0
  142. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +1084 -0
  143. package/src/adapters/drizzle/drizzle-uow-compiler.ts +546 -0
  144. package/src/adapters/drizzle/drizzle-uow-decoder.ts +165 -0
  145. package/src/adapters/drizzle/drizzle-uow-executor.ts +213 -0
  146. package/src/adapters/drizzle/generate.test.ts +643 -0
  147. package/src/adapters/drizzle/generate.ts +481 -0
  148. package/src/adapters/drizzle/join-column-utils.test.ts +79 -0
  149. package/src/adapters/drizzle/join-column-utils.ts +39 -0
  150. package/src/adapters/drizzle/migrate-drizzle.test.ts +226 -0
  151. package/src/adapters/drizzle/shared.ts +22 -0
  152. package/src/adapters/drizzle/test-utils.ts +56 -0
  153. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +789 -0
  154. package/src/adapters/kysely/kysely-adapter.ts +196 -0
  155. package/src/adapters/kysely/kysely-query-builder.test.ts +1344 -0
  156. package/src/adapters/kysely/kysely-query-builder.ts +611 -0
  157. package/src/adapters/kysely/kysely-query-compiler.ts +124 -0
  158. package/src/adapters/kysely/kysely-query.ts +254 -0
  159. package/src/adapters/kysely/kysely-uow-compiler.test.ts +916 -0
  160. package/src/adapters/kysely/kysely-uow-compiler.ts +271 -0
  161. package/src/adapters/kysely/kysely-uow-executor.ts +149 -0
  162. package/src/adapters/kysely/kysely-uow-joins.test.ts +811 -0
  163. package/src/adapters/kysely/migration/execute-mysql.test.ts +1173 -0
  164. package/src/adapters/kysely/migration/execute-postgres.test.ts +2657 -0
  165. package/src/adapters/kysely/migration/execute.ts +382 -0
  166. package/src/adapters/kysely/migration/kysely-migrator.test.ts +197 -0
  167. package/src/fragment.test.ts +287 -0
  168. package/src/fragment.ts +198 -0
  169. package/src/migration-engine/auto-from-schema.test.ts +118 -58
  170. package/src/migration-engine/auto-from-schema.ts +103 -32
  171. package/src/migration-engine/create.test.ts +34 -46
  172. package/src/migration-engine/create.ts +41 -26
  173. package/src/migration-engine/shared.ts +26 -6
  174. package/src/mod.ts +197 -1
  175. package/src/query/condition-builder.test.ts +379 -0
  176. package/src/query/condition-builder.ts +294 -0
  177. package/src/query/cursor.test.ts +296 -0
  178. package/src/query/cursor.ts +147 -0
  179. package/src/query/orm/orm.ts +92 -0
  180. package/src/query/query-type.test.ts +429 -0
  181. package/src/query/query.ts +200 -0
  182. package/src/query/result-transform.test.ts +795 -0
  183. package/src/query/result-transform.ts +247 -0
  184. package/src/query/unit-of-work-types.test.ts +192 -0
  185. package/src/query/unit-of-work.test.ts +947 -0
  186. package/src/query/unit-of-work.ts +1199 -0
  187. package/src/schema/create.test.ts +653 -110
  188. package/src/schema/create.ts +708 -337
  189. package/src/schema/serialize.test.ts +559 -0
  190. package/src/schema/serialize.ts +359 -0
  191. package/src/schema-generator/schema-generator.ts +12 -0
  192. package/src/shared/config.ts +0 -8
  193. package/src/util/import-generator.ts +28 -0
  194. package/src/util/parse.ts +16 -0
  195. package/src/util/types.ts +4 -0
  196. package/tsconfig.json +1 -1
  197. package/tsdown.config.ts +11 -1
  198. package/vitest.config.ts +3 -0
  199. /package/dist/{cuid.js → id.js} +0 -0
  200. /package/src/{cuid.ts → id.ts} +0 -0
@@ -0,0 +1,247 @@
1
+ import type { AnyColumn, AnyTable } from "../schema/create";
2
+ import type { SQLProvider } from "../shared/providers";
3
+ import { deserialize, serialize } from "../schema/serialize";
4
+ import { FragnoId, FragnoReference } from "../schema/create";
5
+ import { createId } from "../id";
6
+
7
+ /**
8
+ * Marker class for reference column values that need subquery resolution.
9
+ * When a reference column receives a string (external ID), this marker tells
10
+ * the query builder to generate a subquery to look up the internal ID.
11
+ * @internal
12
+ */
13
+ export class ReferenceSubquery {
14
+ #referencedTable: AnyTable;
15
+ #externalIdValue: string;
16
+
17
+ constructor(referencedTable: AnyTable, externalIdValue: string) {
18
+ this.#referencedTable = referencedTable;
19
+ this.#externalIdValue = externalIdValue;
20
+ }
21
+
22
+ get referencedTable() {
23
+ return this.#referencedTable;
24
+ }
25
+
26
+ get externalIdValue() {
27
+ return this.#externalIdValue;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Generate a runtime default value for a column that has defaultTo$()
33
+ *
34
+ * Only generates values for runtime defaults (defaultTo$), NOT static defaults (defaultTo).
35
+ * Static defaults should be handled by the database via DEFAULT constraints.
36
+ *
37
+ * @param column - The column with a default value configuration
38
+ * @returns The generated default value, or undefined if the column has no runtime default
39
+ *
40
+ * @internal
41
+ */
42
+ export function generateRuntimeDefault(column: AnyColumn): unknown {
43
+ // Check if column has a default value configuration
44
+ if (!column.default) {
45
+ return undefined;
46
+ }
47
+
48
+ // If it's a static default value (defaultTo), return undefined
49
+ // as the database should handle this via DEFAULT constraint
50
+ if ("value" in column.default) {
51
+ return undefined;
52
+ }
53
+
54
+ // Handle runtime defaults (defaultTo$)
55
+ const runtime = column.default.runtime;
56
+
57
+ if (runtime === "auto") {
58
+ return createId();
59
+ }
60
+
61
+ if (runtime === "now") {
62
+ return new Date();
63
+ }
64
+
65
+ if (typeof runtime === "function") {
66
+ return runtime();
67
+ }
68
+
69
+ return undefined;
70
+ }
71
+
72
+ /**
73
+ * Encodes a record of values from the application format to database format.
74
+ *
75
+ * This function transforms object keys to match SQL column names and serializes
76
+ * values according to the database provider's requirements (e.g., converting
77
+ * JavaScript Date objects to numbers for SQLite).
78
+ *
79
+ * @param values - The record of values to encode in application format
80
+ * @param table - The table schema definition containing column information
81
+ * @param generateDefault - Whether to generate default values for undefined columns
82
+ * @param provider - The SQL provider (sqlite, postgresql, mysql, etc.)
83
+ * @returns A record with database-compatible column names and serialized values
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * const encoded = encodeValues(
88
+ * { userId: 123, createdAt: new Date() },
89
+ * userTable,
90
+ * true,
91
+ * 'sqlite'
92
+ * );
93
+ * // Returns: { user_id: 123, created_at: 1234567890 }
94
+ * ```
95
+ */
96
+ export function encodeValues(
97
+ values: Record<string, unknown>,
98
+ table: AnyTable,
99
+ generateDefault: boolean,
100
+ provider: SQLProvider,
101
+ ): Record<string, unknown> {
102
+ const result: Record<string, unknown> = {};
103
+
104
+ for (const k in table.columns) {
105
+ const col = table.columns[k];
106
+
107
+ // Skip internal ID - never provided by user, auto-generated by database
108
+ if (col.role === "internal-id") {
109
+ continue;
110
+ }
111
+ let value = values[k];
112
+
113
+ if (generateDefault && value === undefined) {
114
+ // Only generate runtime defaults (defaultTo$), not static defaults (defaultTo).
115
+ // Static defaults should be handled by the database via DEFAULT constraints.
116
+ value = generateRuntimeDefault(col);
117
+ }
118
+
119
+ if (value !== undefined) {
120
+ // Handle string references - convert external ID to internal ID via subquery
121
+ if (col.role === "reference" && typeof value === "string") {
122
+ // Find relation that uses this column
123
+ const relation = Object.values(table.relations).find((rel) =>
124
+ rel.on.some(([localCol]) => localCol === k),
125
+ );
126
+ if (relation) {
127
+ result[col.name] = new ReferenceSubquery(relation.table, value);
128
+ continue;
129
+ }
130
+
131
+ throw new Error(`Reference column ${k} not found in table ${table.name}`);
132
+ }
133
+
134
+ result[col.name] = serialize(value, col, provider);
135
+ }
136
+ }
137
+
138
+ return result;
139
+ }
140
+
141
+ /**
142
+ * Decodes a database result record to application format.
143
+ *
144
+ * This function transforms database column names back to application property names
145
+ * and deserializes values according to the database provider's format (e.g., converting
146
+ * SQLite integers back to JavaScript Date objects).
147
+ *
148
+ * Supports relation data encoded with the pattern `relationName:columnName`.
149
+ *
150
+ * @param result - The raw database result record
151
+ * @param table - The table schema definition containing column and relation information
152
+ * @param provider - The SQL provider (sqlite, postgresql, mysql, etc.)
153
+ * @returns A record in application format with deserialized values
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * const decoded = decodeResult(
158
+ * { user_id: 123, created_at: 1234567890, 'posts:title': 'Hello' },
159
+ * userTable,
160
+ * 'sqlite'
161
+ * );
162
+ * // Returns: { userId: 123, createdAt: Date, posts: { title: 'Hello' } }
163
+ * ```
164
+ */
165
+ export function decodeResult(
166
+ result: Record<string, unknown>,
167
+ table: AnyTable,
168
+ provider: SQLProvider,
169
+ ): Record<string, unknown> {
170
+ const output: Record<string, unknown> = {};
171
+ // First pass: collect all column values
172
+ const columnValues: Record<string, unknown> = {};
173
+
174
+ // Collect all relation data (including nested) keyed by relation name
175
+ const relationData: Record<string, Record<string, unknown>> = {};
176
+
177
+ for (const k in result) {
178
+ const colonIndex = k.indexOf(":");
179
+ const value = result[k];
180
+
181
+ // Direct column (no colon)
182
+ if (colonIndex === -1) {
183
+ const col = table.columns[k];
184
+ if (!col) {
185
+ continue;
186
+ }
187
+
188
+ // Store all column values (including hidden ones for FragnoId creation)
189
+ columnValues[k] = deserialize(value, col, provider);
190
+ continue;
191
+ }
192
+
193
+ // Relation column (has colon)
194
+ const relationName = k.slice(0, colonIndex);
195
+ const remainder = k.slice(colonIndex + 1);
196
+
197
+ const relation = table.relations[relationName];
198
+ if (relation === undefined) {
199
+ continue;
200
+ }
201
+
202
+ // Collect relation data with the remaining key path
203
+ relationData[relationName] ??= {};
204
+ relationData[relationName][remainder] = value;
205
+ }
206
+
207
+ // Process each relation's data recursively
208
+ for (const relationName in relationData) {
209
+ const relation = table.relations[relationName];
210
+ if (!relation) {
211
+ continue;
212
+ }
213
+
214
+ // Recursively decode the relation data
215
+ output[relationName] = decodeResult(relationData[relationName], relation.table, provider);
216
+ }
217
+
218
+ // Second pass: create output with FragnoId objects where appropriate
219
+ for (const k in columnValues) {
220
+ const col = table.columns[k];
221
+ if (!col) {
222
+ continue;
223
+ }
224
+
225
+ // Filter out hidden columns (like _internalId, _version) from results
226
+ if (col.isHidden) {
227
+ continue;
228
+ }
229
+
230
+ // For external ID columns, create FragnoId if we have both external and internal IDs
231
+ if (col.role === "external-id" && columnValues["_internalId"] !== undefined) {
232
+ output[k] = new FragnoId({
233
+ externalId: columnValues[k] as string,
234
+ internalId: columnValues["_internalId"] as bigint,
235
+ // _version is always selected as a hidden column, so it should always be present
236
+ version: columnValues["_version"] as number,
237
+ });
238
+ } else if (col.role === "reference") {
239
+ // For reference columns, create FragnoReference with internal ID
240
+ output[k] = FragnoReference.fromInternal(columnValues[k] as bigint);
241
+ } else {
242
+ output[k] = columnValues[k];
243
+ }
244
+ }
245
+
246
+ return output;
247
+ }
@@ -0,0 +1,192 @@
1
+ import { describe, expectTypeOf, it } from "vitest";
2
+ import { column, idColumn, referenceColumn, schema } from "../schema/create";
3
+ import { createUnitOfWork, JoinFindBuilder } from "./unit-of-work";
4
+ import type { FragnoId, FragnoReference } from "../schema/create";
5
+
6
+ type Prettify<T> = {
7
+ [K in keyof T]: T[K];
8
+ } & {};
9
+
10
+ type RecursivePrettify<T> = {
11
+ [K in keyof T]: T[K] extends FragnoId
12
+ ? FragnoId
13
+ : T[K] extends FragnoReference
14
+ ? FragnoReference
15
+ : T[K] extends object
16
+ ? RecursivePrettify<T[K]>
17
+ : T[K];
18
+ } & {};
19
+
20
+ type InferJoinOut<T> =
21
+ T extends JoinFindBuilder<infer _Table, infer _Select, infer JoinOut> ? JoinOut : never;
22
+
23
+ type InferJoinOutPrettify<T> = RecursivePrettify<InferJoinOut<T>>;
24
+
25
+ describe("UnitOfWork type tests", () => {
26
+ const testSchema = schema((s) => {
27
+ return s
28
+ .addTable("users", (t) => {
29
+ return t
30
+ .addColumn("id", idColumn())
31
+ .addColumn("name", column("string"))
32
+ .addColumn("email", column("string"))
33
+ .addColumn("age", column("integer").nullable())
34
+ .addColumn("invitedBy", referenceColumn().nullable())
35
+ .createIndex("idx_email", ["email"], { unique: true })
36
+ .createIndex("idx_name", ["name"]);
37
+ })
38
+ .addTable("posts", (t) => {
39
+ return t
40
+ .addColumn("id", idColumn())
41
+ .addColumn("title", column("string"))
42
+ .addColumn("content", column("string"))
43
+ .addColumn("userId", referenceColumn())
44
+ .createIndex("idx_user", ["userId"])
45
+ .createIndex("idx_title", ["title"]);
46
+ })
47
+ .addTable("comments", (t) => {
48
+ return t
49
+ .addColumn("id", idColumn())
50
+ .addColumn("content", column("string"))
51
+ .addColumn("postId", referenceColumn())
52
+ .addColumn("authorId", referenceColumn())
53
+ .createIndex("idx_post", ["postId"])
54
+ .createIndex("idx_author", ["authorId"]);
55
+ })
56
+ .addReference("author", {
57
+ type: "one",
58
+ from: { table: "posts", column: "userId" },
59
+ to: { table: "users", column: "id" },
60
+ })
61
+ .addReference("inviter", {
62
+ type: "one",
63
+ from: { table: "users", column: "invitedBy" },
64
+ to: { table: "users", column: "id" },
65
+ })
66
+ .addReference("post", {
67
+ type: "one",
68
+ from: { table: "comments", column: "postId" },
69
+ to: { table: "posts", column: "id" },
70
+ })
71
+ .addReference("author", {
72
+ type: "one",
73
+ from: { table: "comments", column: "authorId" },
74
+ to: { table: "users", column: "id" },
75
+ })
76
+ .addReference("posts", {
77
+ type: "many",
78
+ from: { table: "users", column: "id" },
79
+ to: { table: "posts", column: "userId" },
80
+ })
81
+ .addReference("comments", {
82
+ type: "many",
83
+ from: { table: "posts", column: "id" },
84
+ to: { table: "comments", column: "postId" },
85
+ });
86
+ });
87
+
88
+ function createTestUOW() {
89
+ const mockCompiler = {
90
+ compileRetrievalOperation: () => null,
91
+ compileMutationOperation: () => null,
92
+ };
93
+ const mockExecutor = {
94
+ executeRetrievalPhase: async () => [],
95
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
96
+ };
97
+ const mockDecoder = () => [];
98
+ return createUnitOfWork(testSchema, mockCompiler, mockExecutor, mockDecoder);
99
+ }
100
+
101
+ it("should type find without joins correctly", async () => {
102
+ const uow = createTestUOW();
103
+
104
+ const uow1 = uow.find("users", (b) => b.whereIndex("primary"));
105
+ const [_userResult] = await uow1.executeRetrieve();
106
+ type UserResult = RecursivePrettify<(typeof _userResult)[number]>;
107
+
108
+ expectTypeOf<UserResult>().toEqualTypeOf<{
109
+ id: FragnoId;
110
+ name: string;
111
+ email: string;
112
+ age: number | null;
113
+ invitedBy: FragnoReference | null;
114
+ }>();
115
+ });
116
+
117
+ it("should type find with joins correctly", async () => {
118
+ const uow = createTestUOW();
119
+
120
+ const uow1 = uow.find("users", (b) =>
121
+ b.whereIndex("primary").join((jb) => jb.inviter((ib) => ib.select(["name"]))),
122
+ );
123
+ const [_userResult] = await uow1.executeRetrieve();
124
+ type UserResult = RecursivePrettify<(typeof _userResult)[number]>;
125
+
126
+ // @ts-expect-error assert type
127
+ expectTypeOf<UserResult>().toEqualTypeOf<{
128
+ id: FragnoId;
129
+ name: string;
130
+ email: string;
131
+ age: number | null;
132
+ invitedBy: FragnoReference | null;
133
+ inviter: {
134
+ name: string;
135
+ } | null;
136
+ }>();
137
+ });
138
+
139
+ it("join builder without join given", () => {
140
+ const _builder = new JoinFindBuilder("users", testSchema.tables.users);
141
+ type JoinOut = InferJoinOutPrettify<typeof _builder>;
142
+ expectTypeOf<JoinOut>().toEqualTypeOf<{}>();
143
+ });
144
+
145
+ it("join builder with join given", () => {
146
+ const builder = new JoinFindBuilder("users", testSchema.tables.users);
147
+
148
+ /*
149
+ join: (jb) => // jb is IndexedJoinBuilder, the thing with relations as key (fns)
150
+ jb.posts((b) => // b is JoinFindBuilder
151
+ b.whereIndex("primary").select(["id"])
152
+ )
153
+ */
154
+
155
+ const _builderOut = builder.join((jb) => jb.inviter((ib) => ib.select(["name"])));
156
+ type _JoinOut = InferJoinOutPrettify<typeof _builderOut>;
157
+ // ^?
158
+
159
+ type _JoinOutInviter = Prettify<_JoinOut["inviter"]>;
160
+ // ^?
161
+
162
+ // FIXME: There should not be a `{}` in the type
163
+ expectTypeOf<_JoinOutInviter>().toEqualTypeOf<{
164
+ id: FragnoId;
165
+ name: string;
166
+ email: string;
167
+ age: number | null;
168
+ invitedBy: FragnoReference | null;
169
+ } | null>();
170
+ });
171
+
172
+ it("join builder with 'many' relationship returns array", () => {
173
+ const builder = new JoinFindBuilder("users", testSchema.tables.users);
174
+
175
+ const _builderOut = builder.join((jb) => jb.posts((ib) => ib.select(["title"])));
176
+ type _JoinOut = InferJoinOut<typeof _builderOut>;
177
+ // ^?
178
+
179
+ type _JoinOutPosts = Prettify<_JoinOut["posts"]>;
180
+ // ^?
181
+
182
+ // FIXME: There should not be a `{}` in the array
183
+ expectTypeOf<_JoinOutPosts>().toEqualTypeOf<
184
+ {
185
+ id: FragnoId;
186
+ title: string;
187
+ content: string;
188
+ userId: FragnoReference;
189
+ }[]
190
+ >();
191
+ });
192
+ });