@fragno-dev/db 0.2.0 → 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 (62) hide show
  1. package/.turbo/turbo-build.log +34 -30
  2. package/CHANGELOG.md +49 -0
  3. package/dist/adapters/generic-sql/query/where-builder.js +1 -1
  4. package/dist/db-fragment-definition-builder.d.ts +31 -39
  5. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  6. package/dist/db-fragment-definition-builder.js +20 -16
  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 +367 -80
  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 +448 -148
  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 +35 -11
  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 +49 -19
  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 +2 -3
  30. package/dist/schema/create.d.ts.map +1 -1
  31. package/dist/schema/create.js +2 -5
  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/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -1
  38. package/package.json +3 -3
  39. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +1 -0
  40. package/src/adapters/drizzle/drizzle-adapter-sqlite3.test.ts +41 -25
  41. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +39 -25
  42. package/src/db-fragment-definition-builder.test.ts +58 -42
  43. package/src/db-fragment-definition-builder.ts +78 -88
  44. package/src/db-fragment-instantiator.test.ts +64 -88
  45. package/src/db-fragment-integration.test.ts +292 -142
  46. package/src/fragments/internal-fragment.test.ts +272 -266
  47. package/src/fragments/internal-fragment.ts +155 -122
  48. package/src/hooks/hooks.test.ts +268 -264
  49. package/src/hooks/hooks.ts +74 -63
  50. package/src/mod.ts +14 -4
  51. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1582 -998
  52. package/src/query/unit-of-work/execute-unit-of-work.ts +1746 -343
  53. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  54. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +269 -21
  55. package/src/query/unit-of-work/unit-of-work.test.ts +64 -0
  56. package/src/query/unit-of-work/unit-of-work.ts +65 -30
  57. package/src/schema/create.ts +2 -5
  58. package/src/schema/generate-id.test.ts +57 -0
  59. package/src/schema/generate-id.ts +38 -0
  60. package/src/shared/config.ts +0 -10
  61. package/src/shared/connection-pool.ts +0 -24
  62. package/src/shared/prisma.ts +0 -45
@@ -68,153 +68,187 @@ 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
  })
113
124
  .providesService("hookService", ({ defineService }) => {
114
- // TODO(Wilco): re-implement this better
115
125
  return defineService({
116
126
  /**
117
127
  * Get pending hook events for processing.
118
128
  * Returns all pending events for the given namespace that are ready to be processed.
119
129
  */
120
- async getPendingHookEvents(namespace: string): Promise<
121
- {
122
- id: FragnoId;
123
- hookName: string;
124
- payload: unknown;
125
- attempts: number;
126
- maxAttempts: number;
127
- nonce: string;
128
- }[]
129
- > {
130
- const uow = this.uow(internalSchema).find("fragno_hooks", (b) =>
131
- b.whereIndex("idx_namespace_status_retry", (eb) =>
132
- eb.and(eb("namespace", "=", namespace), eb("status", "=", "pending")),
133
- ),
134
- );
135
-
136
- const [events] = await uow.retrievalPhase;
137
-
138
- // Filter for pending status and events ready for retry
139
- const now = new Date();
140
- const ready = events.filter((event) => {
141
- // FIXME(Wilco): this should be handled by the database query, but there seems to be an issue.
142
- if (!event.nextRetryAt) {
143
- return true; // Newly created events (nextRetryAt = null) are ready
144
- }
145
- return event.nextRetryAt <= now; // Only include if retry time has passed
146
- });
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
+ });
147
148
 
148
- return ready.map((event) => ({
149
- id: event.id,
150
- hookName: event.hookName,
151
- payload: event.payload,
152
- attempts: event.attempts,
153
- maxAttempts: event.maxAttempts,
154
- nonce: event.nonce,
155
- }));
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();
156
159
  },
157
160
 
158
161
  /**
159
162
  * Mark a hook event as completed.
160
163
  */
161
- markHookCompleted(eventId: FragnoId): void {
162
- const uow = this.uow(internalSchema);
163
- uow.update("fragno_hooks", eventId, (b) =>
164
- b.set({ status: "completed", lastAttemptAt: new Date() }).check(),
165
- );
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();
166
172
  },
167
173
 
168
174
  /**
169
175
  * Mark a hook event as failed and schedule next retry.
170
176
  */
171
- markHookFailed(
172
- eventId: FragnoId,
173
- error: string,
174
- attempts: number,
175
- retryPolicy: RetryPolicy,
176
- ): void {
177
- const uow = this.uow(internalSchema);
178
-
177
+ markHookFailed(eventId: FragnoId, error: string, attempts: number, retryPolicy: RetryPolicy) {
179
178
  const newAttempts = attempts + 1;
180
179
  const shouldRetry = retryPolicy.shouldRetry(newAttempts - 1);
181
180
 
182
- if (shouldRetry) {
183
- const delayMs = retryPolicy.getDelayMs(newAttempts - 1);
184
- const nextRetryAt = new Date(Date.now() + delayMs);
185
- uow.update("fragno_hooks", eventId, (b) =>
186
- b
187
- .set({
188
- status: "pending",
189
- attempts: newAttempts,
190
- lastAttemptAt: new Date(),
191
- nextRetryAt,
192
- error,
193
- })
194
- .check(),
195
- );
196
- } else {
197
- uow.update("fragno_hooks", eventId, (b) =>
198
- b
199
- .set({
200
- status: "failed",
201
- attempts: newAttempts,
202
- lastAttemptAt: new Date(),
203
- error,
204
- })
205
- .check(),
206
- );
207
- }
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();
208
211
  },
209
212
 
210
213
  /**
211
214
  * Mark a hook event as processing (to prevent concurrent execution).
212
215
  */
213
- markHookProcessing(eventId: FragnoId): void {
214
- const uow = this.uow(internalSchema);
215
- uow.update("fragno_hooks", eventId, (b) =>
216
- b.set({ status: "processing", lastAttemptAt: new Date() }).check(),
217
- );
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();
218
252
  },
219
253
  });
220
254
  })
@@ -233,16 +267,15 @@ export async function getSchemaVersionFromDatabase(
233
267
  namespace: string,
234
268
  ): Promise<number> {
235
269
  try {
236
- const version = await fragment.inContext(async function () {
237
- const version = await this.uow(async ({ executeRetrieve }) => {
238
- const version = fragment.services.settingsService.get(namespace, "schema_version");
239
- await executeRetrieve();
240
- return version;
241
- });
242
-
243
- 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();
244
277
  });
245
- return version;
278
+ return setting ? parseInt(setting.value, 10) : 0;
246
279
  } catch {
247
280
  return 0;
248
281
  }