@fragno-dev/create 0.1.5 → 0.1.7

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fragno-dev/create",
3
3
  "description": "A library for creating Fragno fragments",
4
- "version": "0.1.5",
4
+ "version": "0.1.7",
5
5
  "exports": {
6
6
  ".": {
7
7
  "development": "./src/index.ts",
@@ -14,10 +14,10 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "type": "module",
16
16
  "dependencies": {
17
- "zod": "^4.1.12"
17
+ "zod": "^4.3.5"
18
18
  },
19
19
  "devDependencies": {
20
- "@types/node": "^22",
20
+ "@types/node": "^22.19.7",
21
21
  "@vitest/coverage-istanbul": "^3.2.4",
22
22
  "vitest": "^3.2.4",
23
23
  "@fragno-private/typescript-config": "0.0.1",
@@ -1,18 +1,18 @@
1
1
  import type { BuildTools } from "./index";
2
2
 
3
- const fragnoCoreVersion = "0.1.11";
4
- const fragnoDbVersion = "0.2.0";
5
- const unpluginFragnoVersion = "0.0.7";
6
- const fragnoCliVersion = "0.1.21";
3
+ const fragnoCoreVersion = "0.2.0";
4
+ const fragnoDbVersion = "0.3.0";
5
+ const unpluginFragnoVersion = "0.0.8";
6
+ const fragnoCliVersion = "0.2.0";
7
7
 
8
8
  export const basePkg: Record<string, unknown> = {
9
9
  dependencies: {
10
10
  "@fragno-dev/core": fragnoCoreVersion,
11
- "@standard-schema/spec": "^1.0.0",
12
- zod: "^4.0.5",
11
+ "@standard-schema/spec": "^1.1.0",
12
+ zod: "^4.3.6",
13
13
  },
14
14
  devDependencies: {
15
- "@types/node": "^24",
15
+ "@types/node": "^25.2.2",
16
16
  "@fragno-dev/cli": fragnoCliVersion,
17
17
  "@fragno-dev/unplugin-fragno": unpluginFragnoVersion,
18
18
  },
@@ -38,7 +38,7 @@ export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
38
38
  none: {},
39
39
  tsdown: {
40
40
  devDependencies: {
41
- tsdown: "^0.12.0",
41
+ tsdown: "^0.20.3",
42
42
  },
43
43
  scripts: {
44
44
  build: "tsdown",
@@ -46,7 +46,7 @@ export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
46
46
  },
47
47
  esbuild: {
48
48
  devDependencies: {
49
- esbuild: "^0.25.12",
49
+ esbuild: "^0.27.3",
50
50
  },
51
51
  scripts: {
52
52
  build: "./esbuild.config.js",
@@ -54,7 +54,7 @@ export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
54
54
  },
55
55
  vite: {
56
56
  devDependencies: {
57
- vite: "^6.3.5",
57
+ vite: "^7.3.1",
58
58
  },
59
59
  scripts: {
60
60
  build: "vite build",
@@ -62,10 +62,10 @@ export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
62
62
  },
63
63
  rollup: {
64
64
  devDependencies: {
65
- "@rollup/plugin-node-resolve": "^16.0.2",
66
- "@rollup/plugin-typescript": "^12.1.4",
65
+ "@rollup/plugin-node-resolve": "^16.0.3",
66
+ "@rollup/plugin-typescript": "^12.3.0",
67
67
  tslib: "^2.8.1",
68
- rollup: "^4.41.0",
68
+ rollup: "^4.57.1",
69
69
  },
70
70
  scripts: {
71
71
  build: "rollup -c",
@@ -73,9 +73,9 @@ export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
73
73
  },
74
74
  webpack: {
75
75
  devDependencies: {
76
- webpack: "^5.99.9",
76
+ webpack: "^5.105.0",
77
77
  "webpack-cli": "^6.0.1",
78
- "ts-loader": "^9.5.1",
78
+ "ts-loader": "^9.5.4",
79
79
  },
80
80
  scripts: {
81
81
  build: "webpack",
@@ -83,8 +83,8 @@ export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
83
83
  },
84
84
  rspack: {
85
85
  devDependencies: {
86
- "@rspack/core": "^1.6.1",
87
- "@rspack/cli": "^1.6.1",
86
+ "@rspack/core": "^1.7.5",
87
+ "@rspack/cli": "^1.7.5",
88
88
  },
89
89
  scripts: {
90
90
  build: "rspack build",
@@ -133,7 +133,7 @@ Database schemas are defined in a separate `schema.ts` file using the Fragno sch
133
133
  ```typescript
134
134
  import { column, idColumn, referenceColumn, schema } from "@fragno-dev/db/schema";
135
135
 
136
- export const noteSchema = schema((s) => {
136
+ export const noteSchema = schema("example-fragment", (s) => {
137
137
  return s
138
138
  .addTable("users", (t) => {
139
139
  return t.addColumn("id", idColumn()).addColumn("name", column("string"));
@@ -201,32 +201,43 @@ const fragmentDef = defineFragmentWithDatabase<Config>("my-fragment")
201
201
  - `orm.delete(table, id)` - Delete a row by ID
202
202
  - `.whereIndex(indexName, condition)` - Use indexes for efficient queries
203
203
 
204
- ### Transactions (Unit of Work)
204
+ ### Transactions (handlerTx + serviceTx)
205
205
 
206
- Two-phase pattern for atomic operations (optimistic concurrency control):
206
+ Two-phase pattern for atomic operations (optimistic concurrency control) using handler-owned
207
+ transactions:
207
208
 
208
209
  ```typescript
209
- // Phase 1: Retrieve with version tracking
210
- const uow = orm
211
- .createUnitOfWork()
212
- .find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)))
213
- .find("accounts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
214
- const [users, accounts] = await uow.executeRetrieve();
215
-
216
- // Phase 2: Mutate atomically
217
- uow.update("users", users[0].id, (b) => b.set({ lastLogin: new Date() }).check());
218
- uow.update("accounts", accounts[0].id, (b) =>
219
- b.set({ balance: accounts[0].balance + 100 }).check(),
220
- );
221
-
222
- const { success } = await uow.executeMutations();
223
- if (!success) {
224
- /* Version conflict - retry */
210
+ // Route handler - owns the transaction boundary
211
+ const [result] = await this.handlerTx()
212
+ .withServiceCalls(() => [services.transferFunds({ userId, amount })])
213
+ .execute();
214
+
215
+ // Service - defines the unit of work
216
+ transferFunds: function ({ userId, amount }: { userId: string; amount: number }) {
217
+ return this.serviceTx(mySchema)
218
+ .retrieve((uow) =>
219
+ uow
220
+ .findFirst("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)))
221
+ .findFirst("accounts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId))),
222
+ )
223
+ .mutate(({ uow, retrieveResult: [user, account] }) => {
224
+ if (!user || !account) {
225
+ return { ok: false as const };
226
+ }
227
+
228
+ uow.update("users", user.id, (b) => b.set({ lastLogin: new Date() }).check());
229
+ uow.update("accounts", account.id, (b) =>
230
+ b.set({ balance: account.balance + amount }).check(),
231
+ );
232
+
233
+ return { ok: true as const };
234
+ })
235
+ .build();
225
236
  }
226
237
  ```
227
238
 
228
- **Notes**: `.check()` enables optimistic concurrency control; requires `FragnoId` objects (not
229
- string IDs); use `uow.getCreatedIds()` for new record IDs
239
+ **Notes**: `.check()` enables optimistic concurrency control and requires `FragnoId` objects (not
240
+ string IDs).
230
241
 
231
242
  ## Strategies for Building Fragments
232
243
 
@@ -14,15 +14,18 @@ export default defineConfig([
14
14
  "./src/client/vue.ts",
15
15
  ],
16
16
  dts: true,
17
+ failOnWarn: true,
17
18
  platform: "browser",
18
19
  outDir: "./dist/browser",
19
20
  plugins: [unpluginFragno({ platform: "browser" })],
20
21
  noExternal: [/^@fragno-dev\/core\//],
22
+ inlineOnly: [/^@fragno-dev\/core/, /^nanostores$/, /^@nanostores\//, /^nanoevents$/],
21
23
  },
22
24
  {
23
25
  ignoreWatch: ["./dist"],
24
26
  entry: "./src/index.ts",
25
27
  dts: true,
28
+ failOnWarn: true,
26
29
  platform: "node",
27
30
  outDir: "./dist/node",
28
31
  plugins: [unpluginFragno({ platform: "node" })],
@@ -15,7 +15,10 @@ import { z } from "zod";
15
15
 
16
16
  export interface ExampleConfig {
17
17
  // Add any server-side configuration here if needed
18
- onNoteCreated?: (nonce: string, payload: { noteId: string; userId: string }) => Promise<void>;
18
+ onNoteCreated?: (
19
+ idempotencyKey: string,
20
+ payload: { noteId: string; userId: string },
21
+ ) => Promise<void>;
19
22
  }
20
23
 
21
24
  const exampleFragmentDefinition = defineFragment<ExampleConfig>("example-fragment")
@@ -23,110 +26,104 @@ const exampleFragmentDefinition = defineFragment<ExampleConfig>("example-fragmen
23
26
  .provideHooks(({ defineHook, config }) => ({
24
27
  onNoteCreated: defineHook(async function (payload: { noteId: string; userId: string }) {
25
28
  // Hook runs after transaction commits, with retries on failure
26
- // Use this.nonce for idempotency (available via this context)
27
- await config.onNoteCreated?.(this.nonce, payload);
29
+ // Use this.idempotencyKey for idempotency (e.g., deduplicating webhook calls)
30
+ await config.onNoteCreated?.(this.idempotencyKey, payload);
28
31
  }),
29
32
  }))
30
33
  .providesBaseService(({ defineService }) => {
31
34
  return defineService({
32
- createNote: async function (
35
+ createNote: function (
33
36
  note: Omit<TableToInsertValues<typeof noteSchema.tables.note>, "userId"> & {
34
37
  userId: string;
35
38
  },
36
39
  ) {
37
- const uow = this.uow(noteSchema);
38
-
39
- // Find user first to get FragnoId for reference
40
- const userUow = uow.findFirst("user", (b) =>
41
- b.whereIndex("idx_user_email", (eb) => eb("email", "=", note.userId)),
42
- );
43
- const [user] = await userUow.retrievalPhase;
44
-
45
- if (!user) {
46
- throw new Error("User not found");
47
- }
48
-
49
- // Create note with reference to user
50
- const noteId = uow.create("note", {
51
- content: note.content,
52
- userId: user.id,
53
- });
54
-
55
- // Trigger durable hook (recorded in transaction, executed after commit)
56
- uow.triggerHook("onNoteCreated", {
57
- noteId: noteId.valueOf(),
58
- userId: user.id.valueOf(),
59
- });
60
-
61
- // Wait for handler to execute mutation phase
62
- await uow.mutationPhase;
63
-
64
- return {
65
- id: noteId.valueOf(),
66
- content: note.content,
67
- userId: user.id.valueOf(),
68
- createdAt: new Date(),
69
- };
40
+ return this.serviceTx(noteSchema)
41
+ .retrieve((uow) =>
42
+ uow.findFirst("user", (b) =>
43
+ b.whereIndex("idx_user_email", (eb) => eb("email", "=", note.userId)),
44
+ ),
45
+ )
46
+ .mutate(({ uow, retrieveResult: [user] }) => {
47
+ if (!user) {
48
+ throw new Error("User not found");
49
+ }
50
+
51
+ // Create note with reference to user
52
+ const noteId = uow.create("note", {
53
+ content: note.content,
54
+ userId: user.id,
55
+ });
56
+
57
+ // Trigger durable hook (recorded in transaction, executed after commit)
58
+ uow.triggerHook("onNoteCreated", {
59
+ noteId: noteId.valueOf(),
60
+ userId: user.id.valueOf(),
61
+ });
62
+
63
+ return {
64
+ id: noteId.valueOf(),
65
+ content: note.content,
66
+ userId: user.id.valueOf(),
67
+ createdAt: new Date(),
68
+ };
69
+ })
70
+ .build();
70
71
  },
71
- getNotes: async function () {
72
- const uow = this.uow(noteSchema).find("note", (b) =>
73
- b.whereIndex("primary").join((j) => j.author()),
74
- );
75
- const [notes] = await uow.retrievalPhase;
76
- return notes;
72
+ getNotes: function () {
73
+ return this.serviceTx(noteSchema)
74
+ .retrieve((uow) =>
75
+ uow.find("note", (b) => b.whereIndex("primary").join((j) => j.author())),
76
+ )
77
+ .transformRetrieve(([notes]) => {
78
+ return notes;
79
+ })
80
+ .build();
77
81
  },
78
- getNotesByUser: async function (userEmail: string) {
79
- const uow = this.uow(noteSchema);
80
-
81
- // First find the user by email
82
- const userUow = uow.findFirst("user", (b) =>
83
- b.whereIndex("idx_user_email", (eb) => eb("email", "=", userEmail)),
84
- );
85
- const [user] = await userUow.retrievalPhase;
86
-
87
- if (!user) {
88
- return [];
89
- }
90
-
91
- // Then find notes by the user's FragnoId
92
- // Note: userId is a reference column, FragnoId works directly in whereIndex
93
- const notesUow = uow.find("note", (b) =>
94
- b.whereIndex("idx_note_user", (eb) => eb("userId", "=", user.id)).join((j) => j.author()),
95
- );
96
- const [notes] = await notesUow.retrievalPhase;
97
- return notes;
82
+ getNotesByUser: function (userEmail: string) {
83
+ return this.serviceTx(noteSchema)
84
+ .retrieve((uow) =>
85
+ uow
86
+ .findFirst("user", (b) =>
87
+ b.whereIndex("idx_user_email", (eb) => eb("email", "=", userEmail)),
88
+ )
89
+ .find("note", (b) => b.whereIndex("primary").join((j) => j.author())),
90
+ )
91
+ .transformRetrieve(([user, allNotes]) => {
92
+ if (!user) {
93
+ return [];
94
+ }
95
+ // Filter notes that belong to this user
96
+ return allNotes.filter((note) => note.author?.id.toString() === user.id.toString());
97
+ })
98
+ .build();
98
99
  },
99
- updateNote: async function (noteId: string, content: string) {
100
- const uow = this.uow(noteSchema);
101
-
102
- // Find note first to get FragnoId for optimistic concurrency control
103
- // Join with author to get user information
104
- const noteUow = uow.findFirst("note", (b) =>
105
- b.whereIndex("primary", (eb) => eb("id", "=", noteId)).join((j) => j.author()),
106
- );
107
- const [note] = await noteUow.retrievalPhase;
108
-
109
- if (!note) {
110
- throw new Error("Note not found");
111
- }
112
-
113
- // Update with optimistic concurrency control (.check())
114
- uow.update("note", note.id, (b) => b.set({ content }).check());
115
-
116
- // Wait for handler to execute mutation phase
117
- // On optimistic conflict, handler will retry the whole transaction
118
- await uow.mutationPhase;
119
-
120
- if (!note.author) {
121
- throw new Error("Note author not found");
122
- }
123
-
124
- return {
125
- id: note.id.valueOf(),
126
- content,
127
- userId: note.author.id.valueOf(),
128
- createdAt: note.createdAt,
129
- };
100
+ updateNote: function (noteId: string, content: string) {
101
+ return this.serviceTx(noteSchema)
102
+ .retrieve((uow) =>
103
+ uow.findFirst("note", (b) =>
104
+ b.whereIndex("primary", (eb) => eb("id", "=", noteId)).join((j) => j.author()),
105
+ ),
106
+ )
107
+ .mutate(({ uow, retrieveResult: [note] }) => {
108
+ if (!note) {
109
+ throw new Error("Note not found");
110
+ }
111
+
112
+ if (!note.author) {
113
+ throw new Error("Note author not found");
114
+ }
115
+
116
+ // Update with optimistic concurrency control (.check())
117
+ uow.update("note", note.id, (b) => b.set({ content }).check());
118
+
119
+ return {
120
+ id: note.id.valueOf(),
121
+ content,
122
+ userId: note.author.id.valueOf(),
123
+ createdAt: note.createdAt,
124
+ };
125
+ })
126
+ .build();
130
127
  },
131
128
  });
132
129
  })
@@ -151,18 +148,11 @@ const exampleRoutesFactory = defineRoutes(exampleFragmentDefinition).create(
151
148
  handler: async function ({ query }, { json }) {
152
149
  const userEmail = query.get("userId"); // Using userId param name for backward compatibility
153
150
 
154
- const result = await this.uow(async ({ executeRetrieve }) => {
155
- const notesPromise = userEmail
156
- ? services.getNotesByUser(userEmail)
157
- : services.getNotes();
158
-
159
- // Execute all reads scheduled by services
160
- await executeRetrieve();
161
-
162
- return notesPromise;
163
- });
164
-
165
- const notes = await result;
151
+ const [notes] = await this.handlerTx()
152
+ .withServiceCalls(() => [
153
+ userEmail ? services.getNotesByUser(userEmail) : services.getNotes(),
154
+ ])
155
+ .execute();
166
156
 
167
157
  return json(
168
158
  notes
@@ -192,28 +182,17 @@ const exampleRoutesFactory = defineRoutes(exampleFragmentDefinition).create(
192
182
  handler: async function ({ input }, { json }) {
193
183
  const { content, userEmail } = await input.valid();
194
184
 
195
- // Handler controls transaction execution with retry policy
196
- const result = await this.uow(
197
- async ({ executeMutate }) => {
198
- const notePromise = services.createNote({ content, userId: userEmail });
185
+ const [note] = await this.handlerTx({
186
+ // Retry policy for optimistic concurrency conflicts
187
+ retryPolicy: new ExponentialBackoffRetryPolicy({
188
+ maxRetries: 5,
189
+ initialDelayMs: 10,
190
+ maxDelayMs: 250,
191
+ }),
192
+ })
193
+ .withServiceCalls(() => [services.createNote({ content, userId: userEmail })])
194
+ .execute();
199
195
 
200
- // Execute retrieval (if needed) and mutation atomically
201
- // On optimistic conflict, this whole callback retries
202
- await executeMutate();
203
-
204
- return notePromise;
205
- },
206
- {
207
- // Retry policy for optimistic concurrency conflicts
208
- retryPolicy: new ExponentialBackoffRetryPolicy({
209
- maxRetries: 5,
210
- initialDelayMs: 10,
211
- maxDelayMs: 250,
212
- }),
213
- },
214
- );
215
-
216
- const note = await result;
217
196
  return json(note);
218
197
  },
219
198
  }),
@@ -233,27 +212,16 @@ const exampleRoutesFactory = defineRoutes(exampleFragmentDefinition).create(
233
212
  const { content } = await input.valid();
234
213
  const noteId = pathParams.noteId;
235
214
 
236
- // Handler controls transaction with optimistic concurrency control
237
- const result = await this.uow(
238
- async ({ executeMutate }) => {
239
- const notePromise = services.updateNote(noteId, content);
240
-
241
- // Execute with optimistic concurrency control
242
- // If note was modified, whole transaction retries
243
- await executeMutate();
244
-
245
- return notePromise;
246
- },
247
- {
248
- retryPolicy: new ExponentialBackoffRetryPolicy({
249
- maxRetries: 5,
250
- initialDelayMs: 10,
251
- maxDelayMs: 250,
252
- }),
253
- },
254
- );
215
+ const [note] = await this.handlerTx({
216
+ retryPolicy: new ExponentialBackoffRetryPolicy({
217
+ maxRetries: 5,
218
+ initialDelayMs: 10,
219
+ maxDelayMs: 250,
220
+ }),
221
+ })
222
+ .withServiceCalls(() => [services.updateNote(noteId, content)])
223
+ .execute();
255
224
 
256
- const note = await result;
257
225
  return json(note);
258
226
  },
259
227
  }),
@@ -1,6 +1,6 @@
1
1
  import { column, idColumn, referenceColumn, schema } from "@fragno-dev/db/schema";
2
2
 
3
- export const noteSchema = schema((s) => {
3
+ export const noteSchema = schema("example-fragment", (s) => {
4
4
  return s
5
5
  .addTable("user", (t) => {
6
6
  return t