@pylonsync/create-pylon 0.3.51 → 0.3.54

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 (180) hide show
  1. package/bin/create-pylon.js +347 -1156
  2. package/package.json +4 -3
  3. package/templates/_root/.env.example +9 -0
  4. package/templates/_root/README.md +43 -0
  5. package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
  6. package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
  7. package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
  8. package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
  9. package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
  10. package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
  11. package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
  12. package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
  13. package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
  14. package/templates/backend/b2b/apps/api/package.json +20 -0
  15. package/templates/backend/b2b/apps/api/schema.ts +171 -0
  16. package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
  17. package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
  18. package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
  19. package/templates/backend/barebones/apps/api/package.json +20 -0
  20. package/templates/backend/barebones/apps/api/schema.ts +61 -0
  21. package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
  22. package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
  23. package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
  24. package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
  25. package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
  26. package/templates/backend/chat/apps/api/package.json +20 -0
  27. package/templates/backend/chat/apps/api/schema.ts +93 -0
  28. package/templates/backend/chat/apps/api/tsconfig.json +13 -0
  29. package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
  30. package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
  31. package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
  32. package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
  33. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
  34. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
  35. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
  36. package/templates/backend/consumer/apps/api/package.json +20 -0
  37. package/templates/backend/consumer/apps/api/schema.ts +130 -0
  38. package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
  39. package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
  40. package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
  41. package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
  42. package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
  43. package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
  44. package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
  45. package/templates/backend/todo/apps/api/package.json +20 -0
  46. package/templates/backend/todo/apps/api/schema.ts +85 -0
  47. package/templates/backend/todo/apps/api/tsconfig.json +13 -0
  48. package/templates/expo/barebones/apps/expo/App.tsx +166 -0
  49. package/templates/expo/barebones/apps/expo/app.json +31 -0
  50. package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
  51. package/templates/expo/barebones/apps/expo/package.json +30 -0
  52. package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
  53. package/templates/expo/chat/apps/expo/App.tsx +414 -0
  54. package/templates/expo/chat/apps/expo/app.json +25 -0
  55. package/templates/expo/chat/apps/expo/babel.config.js +6 -0
  56. package/templates/expo/chat/apps/expo/package.json +30 -0
  57. package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
  58. package/templates/expo/consumer/apps/expo/App.tsx +360 -0
  59. package/templates/expo/consumer/apps/expo/app.json +25 -0
  60. package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
  61. package/templates/expo/consumer/apps/expo/package.json +30 -0
  62. package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
  63. package/templates/expo/todo/apps/expo/App.tsx +287 -0
  64. package/templates/expo/todo/apps/expo/app.json +25 -0
  65. package/templates/expo/todo/apps/expo/babel.config.js +6 -0
  66. package/templates/expo/todo/apps/expo/package.json +30 -0
  67. package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
  68. package/templates/ios/barebones/apps/ios/Package.swift +34 -0
  69. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
  70. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  71. package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
  72. package/templates/ios/barebones/apps/ios/project.yml +42 -0
  73. package/templates/ios/chat/apps/ios/Package.swift +34 -0
  74. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
  75. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  76. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  77. package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
  78. package/templates/ios/chat/apps/ios/project.yml +42 -0
  79. package/templates/ios/consumer/apps/ios/Package.swift +23 -0
  80. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  81. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  82. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  83. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  84. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
  85. package/templates/ios/consumer/apps/ios/project.yml +42 -0
  86. package/templates/ios/todo/apps/ios/Package.swift +23 -0
  87. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
  88. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
  89. package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
  90. package/templates/ios/todo/apps/ios/project.yml +32 -0
  91. package/templates/mac/b2b/apps/mac/Package.swift +22 -0
  92. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
  93. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
  94. package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  95. package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  96. package/templates/mac/b2b/apps/mac/project.yml +34 -0
  97. package/templates/mac/barebones/apps/mac/Package.swift +33 -0
  98. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
  99. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
  100. package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  101. package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  102. package/templates/mac/barebones/apps/mac/project.yml +34 -0
  103. package/templates/mac/chat/apps/mac/Package.swift +33 -0
  104. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
  105. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
  106. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
  107. package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
  108. package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  109. package/templates/mac/chat/apps/mac/project.yml +34 -0
  110. package/templates/mac/consumer/apps/mac/Package.swift +33 -0
  111. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
  112. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
  113. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
  114. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
  115. package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
  116. package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  117. package/templates/mac/consumer/apps/mac/project.yml +34 -0
  118. package/templates/mac/todo/apps/mac/Package.swift +33 -0
  119. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
  120. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
  121. package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
  122. package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
  123. package/templates/mac/todo/apps/mac/project.yml +34 -0
  124. package/templates/ui/packages/ui/package.json +26 -0
  125. package/templates/ui/packages/ui/src/button.tsx +44 -0
  126. package/templates/ui/packages/ui/src/card.tsx +39 -0
  127. package/templates/ui/packages/ui/src/cn.ts +12 -0
  128. package/templates/ui/packages/ui/src/index.ts +4 -0
  129. package/templates/ui/packages/ui/src/input.tsx +19 -0
  130. package/templates/ui/packages/ui/tsconfig.json +15 -0
  131. package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
  132. package/templates/web/b2b/apps/web/next.config.ts +24 -0
  133. package/templates/web/b2b/apps/web/package.json +29 -0
  134. package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
  135. package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
  136. package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
  137. package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
  138. package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
  139. package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
  140. package/templates/web/b2b/apps/web/tsconfig.json +26 -0
  141. package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
  142. package/templates/web/barebones/apps/web/next.config.ts +40 -0
  143. package/templates/web/barebones/apps/web/package.json +29 -0
  144. package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
  145. package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
  146. package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
  147. package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
  148. package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
  149. package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
  150. package/templates/web/barebones/apps/web/tsconfig.json +26 -0
  151. package/templates/web/chat/apps/web/next-env.d.ts +2 -0
  152. package/templates/web/chat/apps/web/next.config.ts +24 -0
  153. package/templates/web/chat/apps/web/package.json +29 -0
  154. package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
  155. package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
  156. package/templates/web/chat/apps/web/src/app/globals.css +6 -0
  157. package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
  158. package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
  159. package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
  160. package/templates/web/chat/apps/web/tsconfig.json +26 -0
  161. package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
  162. package/templates/web/consumer/apps/web/next.config.ts +24 -0
  163. package/templates/web/consumer/apps/web/package.json +29 -0
  164. package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
  165. package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
  166. package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
  167. package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
  168. package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
  169. package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
  170. package/templates/web/consumer/apps/web/tsconfig.json +26 -0
  171. package/templates/web/todo/apps/web/next-env.d.ts +2 -0
  172. package/templates/web/todo/apps/web/next.config.ts +24 -0
  173. package/templates/web/todo/apps/web/package.json +32 -0
  174. package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
  175. package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
  176. package/templates/web/todo/apps/web/src/app/globals.css +6 -0
  177. package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
  178. package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
  179. package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
  180. package/templates/web/todo/apps/web/tsconfig.json +26 -0
@@ -0,0 +1,130 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ query,
5
+ action,
6
+ policy,
7
+ buildManifest,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Consumer feed schema. The shape:
12
+ //
13
+ // Profile — public-facing card per User. Handle, display name, bio.
14
+ // Post — one row per post. authorId references the Profile that
15
+ // created it; body holds the text.
16
+ // Like — pivot row: which Profile liked which Post. Composite
17
+ // uniqueness enforced via the ordering of inserts (one
18
+ // row per (profileId, postId) pair).
19
+ //
20
+ // Reads are wide-open by design — feeds are public. Writes require
21
+ // the caller to own the Profile in question (auth.userId == profileId
22
+ // for Profile updates; auth.userId must own a Profile to post).
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const Profile = entity("Profile", {
26
+ userId: field.id("User"),
27
+ handle: field.string(),
28
+ displayName: field.string(),
29
+ bio: field.string().optional(),
30
+ createdAt: field.datetime(),
31
+ });
32
+
33
+ const Post = entity("Post", {
34
+ authorId: field.id("Profile"),
35
+ body: field.string(),
36
+ createdAt: field.datetime(),
37
+ });
38
+
39
+ const Like = entity("Like", {
40
+ profileId: field.id("Profile"),
41
+ postId: field.id("Post"),
42
+ createdAt: field.datetime(),
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Function declarations
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const myProfile = query("myProfile");
50
+ const feed = query("feed");
51
+ const profilePosts = query("profilePosts");
52
+
53
+ const upsertProfile = action("upsertProfile", {
54
+ input: [
55
+ { name: "handle", type: "string" },
56
+ { name: "displayName", type: "string" },
57
+ { name: "bio", type: "string" },
58
+ ],
59
+ });
60
+
61
+ const createPost = action("createPost", {
62
+ input: [{ name: "body", type: "string" }],
63
+ });
64
+
65
+ const deletePost = action("deletePost", {
66
+ input: [{ name: "id", type: "id(Post)" }],
67
+ });
68
+
69
+ const toggleLike = action("toggleLike", {
70
+ input: [{ name: "postId", type: "id(Post)" }],
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Policies — public reads, owner-only writes.
75
+ // ---------------------------------------------------------------------------
76
+
77
+ const profilePolicy = policy({
78
+ name: "profile_public",
79
+ entity: "Profile",
80
+ allowRead: "true",
81
+ allowInsert: "auth.userId == data.userId",
82
+ allowUpdate: "auth.userId == data.userId",
83
+ allowDelete: "auth.userId == data.userId",
84
+ });
85
+
86
+ const postPolicy = policy({
87
+ name: "post_public",
88
+ entity: "Post",
89
+ allowRead: "true",
90
+ // Caller must own a Profile that's being claimed as the author.
91
+ allowInsert:
92
+ "exists(Profile where id = data.authorId and userId = auth.userId)",
93
+ allowUpdate:
94
+ "exists(Profile where id = data.authorId and userId = auth.userId)",
95
+ allowDelete:
96
+ "exists(Profile where id = data.authorId and userId = auth.userId)",
97
+ });
98
+
99
+ const likePolicy = policy({
100
+ name: "like_public",
101
+ entity: "Like",
102
+ allowRead: "true",
103
+ // Caller must own the Profile doing the liking.
104
+ allowInsert:
105
+ "exists(Profile where id = data.profileId and userId = auth.userId)",
106
+ allowUpdate: "false",
107
+ allowDelete:
108
+ "exists(Profile where id = data.profileId and userId = auth.userId)",
109
+ });
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Manifest
113
+ // ---------------------------------------------------------------------------
114
+
115
+ const manifest = buildManifest({
116
+ name: "__APP_NAME_SNAKE__",
117
+ version: "0.0.1",
118
+ entities: [Profile, Post, Like],
119
+ queries: [myProfile, feed, profilePosts],
120
+ actions: [
121
+ upsertProfile,
122
+ createPost,
123
+ deletePost,
124
+ toggleLike,
125
+ ],
126
+ policies: [profilePolicy, postPolicy, likePolicy],
127
+ routes: [],
128
+ });
129
+
130
+ console.log(JSON.stringify(manifest));
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true
11
+ },
12
+ "include": ["schema.ts", "functions/**/*.ts"]
13
+ }
@@ -0,0 +1,27 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Insert a new Todo. Seeds `position` to (max + 1024) so new rows
5
+ * land at the end of the drag-reorder list; the 1024 step leaves
6
+ * room for inserts-between without needing global renumber.
7
+ */
8
+ export default mutation({
9
+ args: { title: v.string() },
10
+ async handler(ctx, args: { title: string }) {
11
+ const existing = await ctx.db.query("Todo", {});
12
+ const maxPos = existing.reduce((acc: number, row: any) => {
13
+ const p =
14
+ typeof row.position === "number"
15
+ ? row.position
16
+ : Date.parse(row.createdAt) || 0;
17
+ return p > acc ? p : acc;
18
+ }, 0);
19
+ const id = await ctx.db.insert("Todo", {
20
+ title: args.title,
21
+ done: false,
22
+ createdAt: new Date().toISOString(),
23
+ position: maxPos + 1024,
24
+ });
25
+ return await ctx.db.get("Todo", id);
26
+ },
27
+ });
@@ -0,0 +1,14 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Remove a Todo row. Returns the row as it existed pre-delete so
5
+ * the client can show a "todo removed" toast or animate it out.
6
+ */
7
+ export default mutation({
8
+ args: { id: v.id("Todo") },
9
+ async handler(ctx, args: { id: string }) {
10
+ const snapshot = await ctx.db.get("Todo", args.id);
11
+ await ctx.db.delete("Todo", args.id);
12
+ return snapshot;
13
+ },
14
+ });
@@ -0,0 +1,16 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Rename a Todo. Trims whitespace; rejects empty titles.
5
+ */
6
+ export default mutation({
7
+ args: { id: v.id("Todo"), title: v.string() },
8
+ async handler(ctx, args: { id: string; title: string }) {
9
+ const trimmed = args.title.trim();
10
+ if (!trimmed) {
11
+ throw ctx.error("EMPTY_TITLE", "title cannot be empty");
12
+ }
13
+ await ctx.db.update("Todo", args.id, { title: trimmed });
14
+ return await ctx.db.get("Todo", args.id);
15
+ },
16
+ });
@@ -0,0 +1,24 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Live query — every Todo, in user-controlled drag-reorder position.
5
+ * Rows without a `position` (legacy data) get sorted by createdAt as
6
+ * a fallback so the list stays deterministic.
7
+ */
8
+ export default query({
9
+ args: {},
10
+ async handler(ctx) {
11
+ const rows = await ctx.db.query("Todo", {});
12
+ return [...rows].sort((a: any, b: any) => {
13
+ const ap =
14
+ typeof a.position === "number"
15
+ ? a.position
16
+ : Date.parse(a.createdAt) || 0;
17
+ const bp =
18
+ typeof b.position === "number"
19
+ ? b.position
20
+ : Date.parse(b.createdAt) || 0;
21
+ return ap - bp;
22
+ });
23
+ },
24
+ });
@@ -0,0 +1,14 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Drag-reorder. Frontend computes `position` as the midpoint of the
5
+ * drop target's neighbors; we just write it. Floats give us ~52 inserts
6
+ * between any two rows before precision matters.
7
+ */
8
+ export default mutation({
9
+ args: { id: v.id("Todo"), position: v.number() },
10
+ async handler(ctx, args: { id: string; position: number }) {
11
+ await ctx.db.update("Todo", args.id, { position: args.position });
12
+ return await ctx.db.get("Todo", args.id);
13
+ },
14
+ });
@@ -0,0 +1,13 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Flip the `done` flag on a Todo. Mutation, not action — needs
5
+ * `ctx.db.update` which is only on writable ctx variants.
6
+ */
7
+ export default mutation({
8
+ args: { id: v.id("Todo"), done: v.bool() },
9
+ async handler(ctx, args: { id: string; done: boolean }) {
10
+ await ctx.db.update("Todo", args.id, { done: args.done });
11
+ return await ctx.db.get("Todo", args.id);
12
+ },
13
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/api",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "pylon dev schema.ts --port 4321",
8
+ "build": "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
9
+ "schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
10
+ "schema:inspect": "pylon schema inspect --sqlite dev.db"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/functions": "^__PYLON_VERSION__"
15
+ },
16
+ "devDependencies": {
17
+ "@pylonsync/cli": "^__PYLON_VERSION__",
18
+ "typescript": "^5.5.0"
19
+ }
20
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ query,
5
+ action,
6
+ policy,
7
+ buildManifest,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Schema
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const Todo = entity("Todo", {
15
+ title: field.string(),
16
+ done: field.bool(),
17
+ createdAt: field.datetime(),
18
+ // Float position so drag-reorder can insert between two existing
19
+ // rows without renumbering the whole list. Frontend computes
20
+ // (prev.position + next.position) / 2 on drop. Optional for
21
+ // backwards compat with legacy rows.
22
+ position: field.float().optional(),
23
+ });
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Function declarations — names only. Implementations live under
27
+ // functions/<name>.ts and are auto-discovered by the runtime.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const listTodos = query("listTodos");
31
+
32
+ const addTodo = action("addTodo", {
33
+ input: [{ name: "title", type: "string" }],
34
+ });
35
+
36
+ const toggleTodo = action("toggleTodo", {
37
+ input: [{ name: "id", type: "id(Todo)" }, { name: "done", type: "bool" }],
38
+ });
39
+
40
+ const deleteTodo = action("deleteTodo", {
41
+ input: [{ name: "id", type: "id(Todo)" }],
42
+ });
43
+
44
+ const editTodo = action("editTodo", {
45
+ input: [
46
+ { name: "id", type: "id(Todo)" },
47
+ { name: "title", type: "string" },
48
+ ],
49
+ });
50
+
51
+ const reorderTodo = action("reorderTodo", {
52
+ input: [
53
+ { name: "id", type: "id(Todo)" },
54
+ { name: "position", type: "float" },
55
+ ],
56
+ });
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Policies — wide-open by default. Tighten for production.
60
+ // ---------------------------------------------------------------------------
61
+
62
+ const todoPolicy = policy({
63
+ name: "todo_open",
64
+ entity: "Todo",
65
+ allowRead: "true",
66
+ allowInsert: "true",
67
+ allowUpdate: "true",
68
+ allowDelete: "true",
69
+ });
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Manifest
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const manifest = buildManifest({
76
+ name: "__APP_NAME_SNAKE__",
77
+ version: "0.0.1",
78
+ entities: [Todo],
79
+ queries: [listTodos],
80
+ actions: [addTodo, toggleTodo, deleteTodo, editTodo, reorderTodo],
81
+ policies: [todoPolicy],
82
+ routes: [],
83
+ });
84
+
85
+ console.log(JSON.stringify(manifest));
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true
11
+ },
12
+ "include": ["schema.ts", "functions/**/*.ts"]
13
+ }
@@ -0,0 +1,166 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TextInput,
6
+ Pressable,
7
+ FlatList,
8
+ ActivityIndicator,
9
+ StyleSheet,
10
+ Platform,
11
+ } from "react-native";
12
+ import { StatusBar } from "expo-status-bar";
13
+ import { init, db, callFn } from "@pylonsync/react-native";
14
+
15
+ // In dev, point at the Pylon control plane on your machine. The iOS
16
+ // simulator can reach `localhost`; an Android emulator uses `10.0.2.2`;
17
+ // a physical device needs your LAN IP. Override via `PYLON_BASE_URL`
18
+ // in app.json `extra` or via an `.env`.
19
+ const PYLON_BASE_URL =
20
+ process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
21
+ (Platform.OS === "android" ? "http://10.0.2.2:4321" : "http://localhost:4321");
22
+
23
+ type Widget = {
24
+ id: string;
25
+ name: string;
26
+ count: number;
27
+ createdAt: string;
28
+ };
29
+
30
+ let initPromise: Promise<void> | null = null;
31
+ function ensureInit() {
32
+ if (!initPromise) {
33
+ initPromise = init({
34
+ baseUrl: PYLON_BASE_URL,
35
+ appName: "__APP_NAME_SNAKE__",
36
+ });
37
+ }
38
+ return initPromise;
39
+ }
40
+
41
+ export default function App() {
42
+ const [ready, setReady] = useState(false);
43
+ useEffect(() => {
44
+ ensureInit().then(() => setReady(true));
45
+ }, []);
46
+
47
+ if (!ready) {
48
+ return (
49
+ <View style={[styles.screen, styles.center]}>
50
+ <ActivityIndicator />
51
+ </View>
52
+ );
53
+ }
54
+ return <Home />;
55
+ }
56
+
57
+ function Home() {
58
+ const { data: widgets, loading } = db.useQuery<Widget>("Widget", {});
59
+ const [name, setName] = useState("");
60
+ const [creating, setCreating] = useState(false);
61
+
62
+ async function create() {
63
+ const trimmed = name.trim();
64
+ if (!trimmed) return;
65
+ setCreating(true);
66
+ try {
67
+ await callFn("createWidget", { name: trimmed });
68
+ setName("");
69
+ } finally {
70
+ setCreating(false);
71
+ }
72
+ }
73
+
74
+ return (
75
+ <View style={styles.screen}>
76
+ <StatusBar style="auto" />
77
+ <Text style={styles.title}>__APP_NAME__</Text>
78
+ <Text style={styles.subtitle}>
79
+ Pylon backend → Expo + React Native, live-updating list.
80
+ </Text>
81
+
82
+ <View style={styles.row}>
83
+ <TextInput
84
+ style={styles.input}
85
+ placeholder="Name a widget…"
86
+ value={name}
87
+ onChangeText={setName}
88
+ editable={!creating}
89
+ onSubmitEditing={create}
90
+ autoCorrect={false}
91
+ />
92
+ <Pressable
93
+ onPress={create}
94
+ disabled={creating || !name.trim()}
95
+ style={({ pressed }) => [
96
+ styles.button,
97
+ (creating || !name.trim()) && styles.buttonDisabled,
98
+ pressed && styles.buttonPressed,
99
+ ]}
100
+ >
101
+ <Text style={styles.buttonLabel}>Create</Text>
102
+ </Pressable>
103
+ </View>
104
+
105
+ {loading ? (
106
+ <ActivityIndicator style={{ marginTop: 24 }} />
107
+ ) : (widgets ?? []).length === 0 ? (
108
+ <Text style={styles.empty}>No widgets yet. Create one above.</Text>
109
+ ) : (
110
+ <FlatList
111
+ data={widgets ?? []}
112
+ keyExtractor={(w) => w.id}
113
+ contentContainerStyle={{ paddingTop: 16 }}
114
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
115
+ renderItem={({ item }) => (
116
+ <View style={styles.item}>
117
+ <Text style={styles.itemName}>{item.name}</Text>
118
+ <Text style={styles.itemCount}>count: {item.count}</Text>
119
+ </View>
120
+ )}
121
+ />
122
+ )}
123
+ </View>
124
+ );
125
+ }
126
+
127
+ const styles = StyleSheet.create({
128
+ screen: {
129
+ flex: 1,
130
+ paddingTop: 64,
131
+ paddingHorizontal: 20,
132
+ backgroundColor: "#fff",
133
+ },
134
+ center: { alignItems: "center", justifyContent: "center" },
135
+ title: { fontSize: 28, fontWeight: "600" },
136
+ subtitle: { color: "#666", marginTop: 4, marginBottom: 20 },
137
+ row: { flexDirection: "row", gap: 8 },
138
+ input: {
139
+ flex: 1,
140
+ borderWidth: 1,
141
+ borderColor: "#d4d4d8",
142
+ borderRadius: 6,
143
+ paddingHorizontal: 12,
144
+ paddingVertical: 8,
145
+ fontSize: 14,
146
+ },
147
+ button: {
148
+ backgroundColor: "#171717",
149
+ borderRadius: 6,
150
+ paddingHorizontal: 16,
151
+ justifyContent: "center",
152
+ },
153
+ buttonDisabled: { opacity: 0.5 },
154
+ buttonPressed: { opacity: 0.8 },
155
+ buttonLabel: { color: "#fff", fontWeight: "600" },
156
+ empty: { textAlign: "center", color: "#999", marginTop: 32 },
157
+ item: {
158
+ flexDirection: "row",
159
+ alignItems: "center",
160
+ justifyContent: "space-between",
161
+ paddingVertical: 12,
162
+ },
163
+ itemName: { fontSize: 15, fontWeight: "500" },
164
+ itemCount: { fontSize: 12, fontFamily: "Menlo", color: "#999" },
165
+ separator: { height: 1, backgroundColor: "#e5e5e5" },
166
+ });
@@ -0,0 +1,31 @@
1
+ {
2
+ "expo": {
3
+ "name": "__APP_NAME__",
4
+ "slug": "__APP_NAME_KEBAB__",
5
+ "version": "0.0.1",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/icon.png",
8
+ "userInterfaceStyle": "automatic",
9
+ "splash": {
10
+ "image": "./assets/splash.png",
11
+ "resizeMode": "contain",
12
+ "backgroundColor": "#ffffff"
13
+ },
14
+ "ios": {
15
+ "supportsTablet": true,
16
+ "bundleIdentifier": "com.example.__APP_NAME_SNAKE__",
17
+ "infoPlist": {
18
+ "NSAppTransportSecurity": {
19
+ "NSAllowsLocalNetworking": true
20
+ }
21
+ }
22
+ },
23
+ "android": {
24
+ "package": "com.example.__APP_NAME_SNAKE__",
25
+ "usesCleartextTraffic": true
26
+ },
27
+ "web": {
28
+ "bundler": "metro"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: ["babel-preset-expo"],
5
+ };
6
+ };
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@__APP_NAME_KEBAB__/expo",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "main": "node_modules/expo/AppEntry.js",
6
+ "scripts": {
7
+ "start": "expo start",
8
+ "android": "expo start --android",
9
+ "ios": "expo start --ios",
10
+ "web": "expo start --web"
11
+ },
12
+ "dependencies": {
13
+ "@pylonsync/sdk": "^__PYLON_VERSION__",
14
+ "@pylonsync/react": "^__PYLON_VERSION__",
15
+ "@pylonsync/react-native": "^__PYLON_VERSION__",
16
+ "@pylonsync/sync": "^__PYLON_VERSION__",
17
+ "@react-native-async-storage/async-storage": "1.23.1",
18
+ "@react-native-community/netinfo": "11.3.1",
19
+ "expo": "~51.0.0",
20
+ "expo-status-bar": "~1.12.1",
21
+ "react": "19.0.0",
22
+ "react-dom": "19.0.0",
23
+ "react-native": "0.74.5"
24
+ },
25
+ "devDependencies": {
26
+ "@babel/core": "^7.20.0",
27
+ "@types/react": "^19.0.0",
28
+ "typescript": "^5.5.0"
29
+ }
30
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["esnext", "dom"],
7
+ "jsx": "react-native",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true,
12
+ "resolveJsonModule": true,
13
+ "noEmit": true
14
+ },
15
+ "include": ["App.tsx", "src/**/*.ts", "src/**/*.tsx"]
16
+ }