@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
@@ -68,45 +68,56 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
68
68
  )
69
69
  .providesService("settingsService", ({ defineService }) => {
70
70
  return defineService({
71
- async get(
72
- namespace: string,
73
- key: string,
74
- ): Promise<{ id: FragnoId; key: string; value: string } | undefined> {
71
+ /**
72
+ * Get a setting by namespace and key.
73
+ */
74
+ get(namespace: string, key: string) {
75
75
  const fullKey = `${namespace}.${key}`;
76
- const uow = this.uow(internalSchema).find(SETTINGS_TABLE_NAME, (b) =>
77
- b.whereIndex("unique_key", (eb) => eb("key", "=", fullKey)),
78
- );
79
- const [results] = await uow.retrievalPhase;
80
- return results?.[0];
76
+ return this.serviceTx(internalSchema)
77
+ .retrieve((uow) =>
78
+ uow.findFirst(SETTINGS_TABLE_NAME, (b) =>
79
+ b.whereIndex("unique_key", (eb) => eb("key", "=", fullKey)),
80
+ ),
81
+ )
82
+ .transformRetrieve(
83
+ ([result]): { id: FragnoId; key: string; value: string } | undefined =>
84
+ result ?? undefined,
85
+ )
86
+ .build();
81
87
  },
82
88
 
83
- async set(namespace: string, key: string, value: string) {
89
+ /**
90
+ * Set a setting value by namespace and key.
91
+ */
92
+ set(namespace: string, key: string, value: string) {
84
93
  const fullKey = `${namespace}.${key}`;
85
- const uow = this.uow(internalSchema);
86
-
87
- // First, find if the key already exists
88
- const findUow = uow.find(SETTINGS_TABLE_NAME, (b) =>
89
- b.whereIndex("unique_key", (eb) => eb("key", "=", fullKey)),
90
- );
91
- const [existing] = await findUow.retrievalPhase;
92
-
93
- if (existing?.[0]) {
94
- uow.update(SETTINGS_TABLE_NAME, existing[0].id, (b) => b.set({ value }).check());
95
- } else {
96
- uow.create(SETTINGS_TABLE_NAME, {
97
- key: fullKey,
98
- value,
99
- });
100
- }
101
-
102
- // Await mutation phase - will throw if mutation fails
103
- await uow.mutationPhase;
94
+ return this.serviceTx(internalSchema)
95
+ .retrieve((uow) =>
96
+ uow.findFirst(SETTINGS_TABLE_NAME, (b) =>
97
+ b.whereIndex("unique_key", (eb) => eb("key", "=", fullKey)),
98
+ ),
99
+ )
100
+ .transformRetrieve(([result]) => result)
101
+ .mutate(({ uow, retrieveResult }) => {
102
+ if (retrieveResult) {
103
+ uow.update(SETTINGS_TABLE_NAME, retrieveResult.id, (b) => b.set({ value }).check());
104
+ } else {
105
+ uow.create(SETTINGS_TABLE_NAME, {
106
+ key: fullKey,
107
+ value,
108
+ });
109
+ }
110
+ })
111
+ .build();
104
112
  },
105
113
 
106
- async delete(id: FragnoId) {
107
- const uow = this.uow(internalSchema);
108
- uow.delete(SETTINGS_TABLE_NAME, id);
109
- await uow.mutationPhase;
114
+ /**
115
+ * Delete a setting by ID.
116
+ */
117
+ delete(id: FragnoId) {
118
+ return this.serviceTx(internalSchema)
119
+ .mutate(({ uow }) => uow.delete(SETTINGS_TABLE_NAME, id))
120
+ .build();
110
121
  },
111
122
  });
112
123
  })
@@ -116,104 +127,128 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
116
127
  * Get pending hook events for processing.
117
128
  * Returns all pending events for the given namespace that are ready to be processed.
118
129
  */
119
- async getPendingHookEvents(namespace: string): Promise<
120
- {
121
- id: FragnoId;
122
- hookName: string;
123
- payload: unknown;
124
- attempts: number;
125
- maxAttempts: number;
126
- nonce: string;
127
- }[]
128
- > {
129
- const uow = this.uow(internalSchema).find("fragno_hooks", (b) =>
130
- b.whereIndex("idx_namespace_status_retry", (eb) =>
131
- eb.and(eb("namespace", "=", namespace), eb("status", "=", "pending")),
132
- ),
133
- );
134
-
135
- const [events] = await uow.retrievalPhase;
136
-
137
- // Filter for pending status and events ready for retry
138
- const now = new Date();
139
- const ready = events.filter((event) => {
140
- // FIXME(Wilco): this should be handled by the database query, but there seems to be an issue.
141
- if (!event.nextRetryAt) {
142
- return true; // Newly created events (nextRetryAt = null) are ready
143
- }
144
- return event.nextRetryAt <= now; // Only include if retry time has passed
145
- });
130
+ getPendingHookEvents(namespace: string) {
131
+ return this.serviceTx(internalSchema)
132
+ .retrieve((uow) =>
133
+ uow.find("fragno_hooks", (b) =>
134
+ b.whereIndex("idx_namespace_status_retry", (eb) =>
135
+ eb.and(eb("namespace", "=", namespace), eb("status", "=", "pending")),
136
+ ),
137
+ ),
138
+ )
139
+ .transformRetrieve(([events]) => {
140
+ const now = new Date();
141
+ // FIXME(Wilco): this should be handled by the database query, but there seems to be an issue.
142
+ const ready = events.filter((event) => {
143
+ if (!event.nextRetryAt) {
144
+ return true; // Newly created events (nextRetryAt = null) are ready
145
+ }
146
+ return event.nextRetryAt <= now; // Only include if retry time has passed
147
+ });
146
148
 
147
- return ready.map((event) => ({
148
- id: event.id,
149
- hookName: event.hookName,
150
- payload: event.payload,
151
- attempts: event.attempts,
152
- maxAttempts: event.maxAttempts,
153
- nonce: event.nonce,
154
- }));
149
+ return ready.map((event) => ({
150
+ id: event.id,
151
+ hookName: event.hookName,
152
+ payload: event.payload as unknown,
153
+ attempts: event.attempts,
154
+ maxAttempts: event.maxAttempts,
155
+ idempotencyKey: event.nonce,
156
+ }));
157
+ })
158
+ .build();
155
159
  },
156
160
 
157
161
  /**
158
162
  * Mark a hook event as completed.
159
163
  */
160
- markHookCompleted(eventId: FragnoId): void {
161
- const uow = this.uow(internalSchema);
162
- uow.update("fragno_hooks", eventId, (b) =>
163
- b.set({ status: "completed", lastAttemptAt: new Date() }).check(),
164
- );
164
+ markHookCompleted(eventId: FragnoId) {
165
+ return this.serviceTx(internalSchema)
166
+ .mutate(({ uow }) =>
167
+ uow.update("fragno_hooks", eventId, (b) =>
168
+ b.set({ status: "completed", lastAttemptAt: new Date() }).check(),
169
+ ),
170
+ )
171
+ .build();
165
172
  },
166
173
 
167
174
  /**
168
175
  * Mark a hook event as failed and schedule next retry.
169
176
  */
170
- markHookFailed(
171
- eventId: FragnoId,
172
- error: string,
173
- attempts: number,
174
- retryPolicy: RetryPolicy,
175
- ): void {
176
- const uow = this.uow(internalSchema);
177
-
177
+ markHookFailed(eventId: FragnoId, error: string, attempts: number, retryPolicy: RetryPolicy) {
178
178
  const newAttempts = attempts + 1;
179
179
  const shouldRetry = retryPolicy.shouldRetry(newAttempts - 1);
180
180
 
181
- if (shouldRetry) {
182
- const delayMs = retryPolicy.getDelayMs(newAttempts - 1);
183
- const nextRetryAt = new Date(Date.now() + delayMs);
184
- uow.update("fragno_hooks", eventId, (b) =>
185
- b
186
- .set({
187
- status: "pending",
188
- attempts: newAttempts,
189
- lastAttemptAt: new Date(),
190
- nextRetryAt,
191
- error,
192
- })
193
- .check(),
194
- );
195
- } else {
196
- uow.update("fragno_hooks", eventId, (b) =>
197
- b
198
- .set({
199
- status: "failed",
200
- attempts: newAttempts,
201
- lastAttemptAt: new Date(),
202
- error,
203
- })
204
- .check(),
205
- );
206
- }
181
+ return this.serviceTx(internalSchema)
182
+ .mutate(({ uow }) => {
183
+ if (shouldRetry) {
184
+ const delayMs = retryPolicy.getDelayMs(newAttempts - 1);
185
+ const nextRetryAt = new Date(Date.now() + delayMs);
186
+ uow.update("fragno_hooks", eventId, (b) =>
187
+ b
188
+ .set({
189
+ status: "pending",
190
+ attempts: newAttempts,
191
+ lastAttemptAt: new Date(),
192
+ nextRetryAt,
193
+ error,
194
+ })
195
+ .check(),
196
+ );
197
+ } else {
198
+ uow.update("fragno_hooks", eventId, (b) =>
199
+ b
200
+ .set({
201
+ status: "failed",
202
+ attempts: newAttempts,
203
+ lastAttemptAt: new Date(),
204
+ error,
205
+ })
206
+ .check(),
207
+ );
208
+ }
209
+ })
210
+ .build();
207
211
  },
208
212
 
209
213
  /**
210
214
  * Mark a hook event as processing (to prevent concurrent execution).
211
215
  */
212
- markHookProcessing(eventId: FragnoId): void {
213
- const uow = this.uow(internalSchema);
214
- uow.update("fragno_hooks", eventId, (b) =>
215
- b.set({ status: "processing", lastAttemptAt: new Date() }).check(),
216
- );
216
+ markHookProcessing(eventId: FragnoId) {
217
+ return this.serviceTx(internalSchema)
218
+ .mutate(({ uow }) =>
219
+ uow.update("fragno_hooks", eventId, (b) =>
220
+ b.set({ status: "processing", lastAttemptAt: new Date() }).check(),
221
+ ),
222
+ )
223
+ .build();
224
+ },
225
+
226
+ /**
227
+ * Get a hook event by ID (for testing/verification purposes).
228
+ */
229
+ getHookById(eventId: FragnoId) {
230
+ return this.serviceTx(internalSchema)
231
+ .retrieve((uow) =>
232
+ uow.findFirst("fragno_hooks", (b) =>
233
+ b.whereIndex("primary", (eb) => eb("id", "=", eventId)),
234
+ ),
235
+ )
236
+ .transformRetrieve(([result]) => result ?? undefined)
237
+ .build();
238
+ },
239
+
240
+ /**
241
+ * Get all hook events for a namespace (for testing/verification purposes).
242
+ */
243
+ getHooksByNamespace(namespace: string) {
244
+ return this.serviceTx(internalSchema)
245
+ .retrieve((uow) =>
246
+ uow.find("fragno_hooks", (b) =>
247
+ b.whereIndex("idx_namespace_status_retry", (eb) => eb("namespace", "=", namespace)),
248
+ ),
249
+ )
250
+ .transformRetrieve(([events]) => events)
251
+ .build();
217
252
  },
218
253
  });
219
254
  })
@@ -232,16 +267,15 @@ export async function getSchemaVersionFromDatabase(
232
267
  namespace: string,
233
268
  ): Promise<number> {
234
269
  try {
235
- const version = await fragment.inContext(async function () {
236
- const version = await this.uow(async ({ executeRetrieve }) => {
237
- const version = fragment.services.settingsService.get(namespace, "schema_version");
238
- await executeRetrieve();
239
- return version;
240
- });
241
-
242
- return version ? parseInt(version.value, 10) : 0;
270
+ const setting = await fragment.inContext(async function () {
271
+ return await this.handlerTx()
272
+ .withServiceCalls(
273
+ () => [fragment.services.settingsService.get(namespace, "schema_version")] as const,
274
+ )
275
+ .transform(({ serviceResult: [result] }) => result)
276
+ .execute();
243
277
  });
244
- return version;
278
+ return setting ? parseInt(setting.value, 10) : 0;
245
279
  } catch {
246
280
  return 0;
247
281
  }