@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,61 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ query,
5
+ action,
6
+ policy,
7
+ buildManifest,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Schema — one entity, two operations. The smallest thing that
12
+ // demonstrates the loop: declare → handler → live query.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const Widget = entity("Widget", {
16
+ name: field.string(),
17
+ count: field.int(),
18
+ createdAt: field.datetime(),
19
+ });
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Function declarations — names only. Implementations live under
23
+ // functions/<name>.ts and are auto-discovered by the runtime.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const listWidgets = query("listWidgets");
27
+
28
+ const createWidget = action("createWidget", {
29
+ input: [{ name: "name", type: "string" }],
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Policies — wide-open by default. Tighten for production.
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const widgetPolicy = policy({
37
+ name: "widget_open",
38
+ entity: "Widget",
39
+ allowRead: "true",
40
+ allowInsert: "true",
41
+ allowUpdate: "true",
42
+ allowDelete: "true",
43
+ });
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Manifest — pylon codegen reads this and emits pylon.manifest.json.
47
+ // pylon dev / pylon codegen run `bun run schema.ts` and read the
48
+ // manifest off stdout — every entry file ends with this console.log.
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const manifest = buildManifest({
52
+ name: "__APP_NAME_SNAKE__",
53
+ version: "0.0.1",
54
+ entities: [Widget],
55
+ queries: [listWidgets],
56
+ actions: [createWidget],
57
+ policies: [widgetPolicy],
58
+ routes: [],
59
+ });
60
+
61
+ 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,32 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Create a chat room. Slug must be unique (lowercase + dashes).
5
+ */
6
+ export default mutation({
7
+ args: { slug: v.string(), name: v.string() },
8
+ async handler(ctx, args: { slug: string; name: string }) {
9
+ if (!ctx.auth.userId) {
10
+ throw ctx.error("UNAUTHENTICATED", "log in first");
11
+ }
12
+ const slug = args.slug.trim().toLowerCase();
13
+ if (!/^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/.test(slug)) {
14
+ throw ctx.error(
15
+ "INVALID_SLUG",
16
+ "slug must be 3–32 chars: lowercase letters, digits, dashes",
17
+ );
18
+ }
19
+ const name = args.name.trim();
20
+ if (!name) throw ctx.error("EMPTY_NAME", "room name cannot be empty");
21
+ const dup = (await ctx.db.query("Room", { slug })) as any[];
22
+ if (dup.length > 0) {
23
+ throw ctx.error("SLUG_TAKEN", `room slug "${slug}" is taken`);
24
+ }
25
+ const id = await ctx.db.insert("Room", {
26
+ slug,
27
+ name,
28
+ createdAt: new Date().toISOString(),
29
+ });
30
+ return await ctx.db.get("Room", id);
31
+ },
32
+ });
@@ -0,0 +1,15 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Every room, oldest first (so the General room created on first run
5
+ * stays at the top regardless of activity).
6
+ */
7
+ export default query({
8
+ args: {},
9
+ async handler(ctx) {
10
+ const rows = (await ctx.db.query("Room", {})) as any[];
11
+ return rows.sort(
12
+ (a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt),
13
+ );
14
+ },
15
+ });
@@ -0,0 +1,20 @@
1
+ import { query, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Last 200 messages in a room, oldest first (chat-conventional —
5
+ * scroll up for history). For a real chat app, paginate via cursor;
6
+ * the scaffold caps at 200 since rendering more in a single FlatList
7
+ * / SwiftUI List without virtualization gets expensive.
8
+ */
9
+ export default query({
10
+ args: { roomId: v.id("Room") },
11
+ async handler(ctx, args: { roomId: string }) {
12
+ const rows = (await ctx.db.query("Message", {
13
+ roomId: args.roomId,
14
+ })) as any[];
15
+ const sorted = [...rows].sort(
16
+ (a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt),
17
+ );
18
+ return sorted.slice(-200);
19
+ },
20
+ });
@@ -0,0 +1,37 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Append a message. authorId comes from `ctx.auth.userId`; the
5
+ * authorName the client passes is denormalized into the row so we
6
+ * don't have to join Profile (or any equivalent table) on every
7
+ * read. For real apps, swap this for a Profile join — denormalized
8
+ * names go stale when the user renames themselves.
9
+ */
10
+ export default mutation({
11
+ args: {
12
+ roomId: v.id("Room"),
13
+ body: v.string(),
14
+ authorName: v.string(),
15
+ },
16
+ async handler(
17
+ ctx,
18
+ args: { roomId: string; body: string; authorName: string },
19
+ ) {
20
+ if (!ctx.auth.userId) {
21
+ throw ctx.error("UNAUTHENTICATED", "log in first");
22
+ }
23
+ const body = args.body.trim();
24
+ if (!body) throw ctx.error("EMPTY_BODY", "message body cannot be empty");
25
+ if (body.length > 2000) {
26
+ throw ctx.error("TOO_LONG", "message capped at 2000 chars");
27
+ }
28
+ const id = await ctx.db.insert("Message", {
29
+ roomId: args.roomId,
30
+ authorId: ctx.auth.userId,
31
+ authorName: args.authorName.trim() || "anonymous",
32
+ body,
33
+ createdAt: new Date().toISOString(),
34
+ });
35
+ return await ctx.db.get("Message", id);
36
+ },
37
+ });
@@ -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,93 @@
1
+ import {
2
+ entity,
3
+ field,
4
+ query,
5
+ action,
6
+ policy,
7
+ buildManifest,
8
+ } from "@pylonsync/sdk";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Realtime chat schema. The shape:
12
+ //
13
+ // Room — a chat channel. slug + name.
14
+ // Message — one row per message. roomId + authorId + body.
15
+ //
16
+ // Public reads inside a room (anyone can browse). Writes require a
17
+ // signed-in user. For private rooms in production, add a Membership
18
+ // entity and gate Room reads on it (mirrors the b2b pattern).
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const Room = entity("Room", {
22
+ slug: field.string(),
23
+ name: field.string(),
24
+ createdAt: field.datetime(),
25
+ });
26
+
27
+ const Message = entity("Message", {
28
+ roomId: field.id("Room"),
29
+ authorId: field.id("User"),
30
+ authorName: field.string(),
31
+ body: field.string(),
32
+ createdAt: field.datetime(),
33
+ });
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Function declarations
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const listRooms = query("listRooms");
40
+ const roomMessages = query("roomMessages");
41
+
42
+ const createRoom = action("createRoom", {
43
+ input: [
44
+ { name: "slug", type: "string" },
45
+ { name: "name", type: "string" },
46
+ ],
47
+ });
48
+
49
+ const sendMessage = action("sendMessage", {
50
+ input: [
51
+ { name: "roomId", type: "id(Room)" },
52
+ { name: "body", type: "string" },
53
+ { name: "authorName", type: "string" },
54
+ ],
55
+ });
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Policies — wide-open reads, signed-in writes.
59
+ // ---------------------------------------------------------------------------
60
+
61
+ const roomPolicy = policy({
62
+ name: "room_public",
63
+ entity: "Room",
64
+ allowRead: "true",
65
+ allowInsert: "auth.userId != null",
66
+ allowUpdate: "false",
67
+ allowDelete: "false",
68
+ });
69
+
70
+ const messagePolicy = policy({
71
+ name: "message_public",
72
+ entity: "Message",
73
+ allowRead: "true",
74
+ allowInsert: "auth.userId != null and data.authorId == auth.userId",
75
+ allowUpdate: "false",
76
+ allowDelete: "data.authorId == auth.userId",
77
+ });
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Manifest
81
+ // ---------------------------------------------------------------------------
82
+
83
+ const manifest = buildManifest({
84
+ name: "__APP_NAME_SNAKE__",
85
+ version: "0.0.1",
86
+ entities: [Room, Message],
87
+ queries: [listRooms, roomMessages],
88
+ actions: [createRoom, sendMessage],
89
+ policies: [roomPolicy, messagePolicy],
90
+ routes: [],
91
+ });
92
+
93
+ 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,48 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Post something. Resolves the caller's Profile, then writes a Post
5
+ * with `authorId = profile.id`. Returns the inserted row + author
6
+ * card so the client can render it without a follow-up read.
7
+ */
8
+ export default mutation({
9
+ args: { body: v.string() },
10
+ async handler(ctx, args: { body: string }) {
11
+ if (!ctx.auth.userId) {
12
+ throw ctx.error("UNAUTHENTICATED", "log in first");
13
+ }
14
+ const body = args.body.trim();
15
+ if (!body) throw ctx.error("EMPTY_BODY", "post body cannot be empty");
16
+ if (body.length > 1000) {
17
+ throw ctx.error("TOO_LONG", "post body capped at 1000 chars");
18
+ }
19
+ const profiles = (await ctx.db.query("Profile", {
20
+ userId: ctx.auth.userId,
21
+ })) as any[];
22
+ if (profiles.length === 0) {
23
+ throw ctx.error(
24
+ "NO_PROFILE",
25
+ "create a profile first via upsertProfile",
26
+ );
27
+ }
28
+ const profile = profiles[0];
29
+ const id = await ctx.db.insert("Post", {
30
+ authorId: profile.id,
31
+ body,
32
+ createdAt: new Date().toISOString(),
33
+ });
34
+ const post = (await ctx.db.get("Post", id)) as any;
35
+ return {
36
+ id: post.id,
37
+ body: post.body,
38
+ createdAt: post.createdAt,
39
+ author: {
40
+ id: profile.id,
41
+ handle: profile.handle,
42
+ displayName: profile.displayName,
43
+ },
44
+ likeCount: 0,
45
+ likedByMe: false,
46
+ };
47
+ },
48
+ });
@@ -0,0 +1,21 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Delete a Post and any associated Likes. Post policy already gates
5
+ * deletes to the author's Profile, so a non-author can't reach the
6
+ * actual delete call — but we double-check so a future policy
7
+ * loosening doesn't accidentally orphan Like rows.
8
+ */
9
+ export default mutation({
10
+ args: { id: v.id("Post") },
11
+ async handler(ctx, args: { id: string }) {
12
+ const snapshot = (await ctx.db.get("Post", args.id)) as any;
13
+ if (!snapshot) throw ctx.error("NOT_FOUND", "post not found");
14
+ const likes = (await ctx.db.query("Like", { postId: args.id })) as any[];
15
+ for (const like of likes) {
16
+ await ctx.db.delete("Like", like.id);
17
+ }
18
+ await ctx.db.delete("Post", args.id);
19
+ return snapshot;
20
+ },
21
+ });
@@ -0,0 +1,57 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Public feed — every Post, newest first. Joins author Profile + the
5
+ * caller's like state into the response so the client can render a
6
+ * row without N+1 follow-up queries.
7
+ *
8
+ * For a real consumer app, paginate this (limit + cursor by createdAt)
9
+ * — the scaffold returns at most 100 rows, which is plenty for a
10
+ * Hello-World feed but unsustainable past a few thousand posts.
11
+ */
12
+ export default query({
13
+ args: {},
14
+ async handler(ctx) {
15
+ const posts = (await ctx.db.query("Post", {})) as any[];
16
+ const sorted = [...posts]
17
+ .sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt))
18
+ .slice(0, 100);
19
+
20
+ const callerProfile = ctx.auth.userId
21
+ ? ((await ctx.db.query("Profile", {
22
+ userId: ctx.auth.userId,
23
+ })) as any[])[0]
24
+ : null;
25
+
26
+ const out: Array<{
27
+ id: string;
28
+ body: string;
29
+ createdAt: string;
30
+ author: { id: string; handle: string; displayName: string } | null;
31
+ likeCount: number;
32
+ likedByMe: boolean;
33
+ }> = [];
34
+ for (const p of sorted) {
35
+ const author = (await ctx.db.get("Profile", p.authorId)) as any;
36
+ const likes = (await ctx.db.query("Like", { postId: p.id })) as any[];
37
+ const likedByMe = callerProfile
38
+ ? likes.some((l) => l.profileId === callerProfile.id)
39
+ : false;
40
+ out.push({
41
+ id: p.id,
42
+ body: p.body,
43
+ createdAt: p.createdAt,
44
+ author: author
45
+ ? {
46
+ id: author.id,
47
+ handle: author.handle,
48
+ displayName: author.displayName,
49
+ }
50
+ : null,
51
+ likeCount: likes.length,
52
+ likedByMe,
53
+ });
54
+ }
55
+ return out;
56
+ },
57
+ });
@@ -0,0 +1,17 @@
1
+ import { query } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Resolve the caller's own Profile (or null if they haven't created
5
+ * one yet). Used by the client to decide whether to show the
6
+ * profile-setup screen on first launch.
7
+ */
8
+ export default query({
9
+ args: {},
10
+ async handler(ctx) {
11
+ if (!ctx.auth.userId) return null;
12
+ const rows = (await ctx.db.query("Profile", {
13
+ userId: ctx.auth.userId,
14
+ })) as any[];
15
+ return rows[0] ?? null;
16
+ },
17
+ });
@@ -0,0 +1,17 @@
1
+ import { query, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Every Post by a single Profile, newest first. Used by the profile
5
+ * page on each platform.
6
+ */
7
+ export default query({
8
+ args: { profileId: v.id("Profile") },
9
+ async handler(ctx, args: { profileId: string }) {
10
+ const rows = (await ctx.db.query("Post", {
11
+ authorId: args.profileId,
12
+ })) as any[];
13
+ return rows.sort(
14
+ (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt),
15
+ );
16
+ },
17
+ });
@@ -0,0 +1,48 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Toggle a like on a Post. Idempotent — calling twice from the same
5
+ * user lands at the original state. Returns { liked: boolean,
6
+ * likeCount: number } so the client can update its UI without a
7
+ * follow-up read.
8
+ */
9
+ export default mutation({
10
+ args: { postId: v.id("Post") },
11
+ async handler(ctx, args: { postId: string }) {
12
+ if (!ctx.auth.userId) {
13
+ throw ctx.error("UNAUTHENTICATED", "log in first");
14
+ }
15
+ const profiles = (await ctx.db.query("Profile", {
16
+ userId: ctx.auth.userId,
17
+ })) as any[];
18
+ if (profiles.length === 0) {
19
+ throw ctx.error("NO_PROFILE", "create a profile first");
20
+ }
21
+ const profile = profiles[0];
22
+
23
+ const existing = (await ctx.db.query("Like", {
24
+ postId: args.postId,
25
+ profileId: profile.id,
26
+ })) as any[];
27
+
28
+ if (existing.length === 0) {
29
+ await ctx.db.insert("Like", {
30
+ postId: args.postId,
31
+ profileId: profile.id,
32
+ createdAt: new Date().toISOString(),
33
+ });
34
+ } else {
35
+ for (const like of existing) {
36
+ await ctx.db.delete("Like", like.id);
37
+ }
38
+ }
39
+
40
+ const allLikes = (await ctx.db.query("Like", {
41
+ postId: args.postId,
42
+ })) as any[];
43
+ return {
44
+ liked: existing.length === 0,
45
+ likeCount: allLikes.length,
46
+ };
47
+ },
48
+ });
@@ -0,0 +1,70 @@
1
+ import { mutation, v } from "@pylonsync/functions";
2
+
3
+ /**
4
+ * Create-or-update the caller's Profile. Handle is unique (case-
5
+ * insensitive); we lowercase + check before inserting. On update
6
+ * we don't re-check the handle if it didn't change.
7
+ */
8
+ export default mutation({
9
+ args: {
10
+ handle: v.string(),
11
+ displayName: v.string(),
12
+ bio: v.string(),
13
+ },
14
+ async handler(
15
+ ctx,
16
+ args: { handle: string; displayName: string; bio: string },
17
+ ) {
18
+ if (!ctx.auth.userId) {
19
+ throw ctx.error("UNAUTHENTICATED", "log in first");
20
+ }
21
+ const handle = args.handle.trim().toLowerCase();
22
+ if (!/^[a-z0-9_]{2,20}$/.test(handle)) {
23
+ throw ctx.error(
24
+ "INVALID_HANDLE",
25
+ "handle must be 2–20 chars: lowercase letters, digits, underscore",
26
+ );
27
+ }
28
+ const displayName = args.displayName.trim();
29
+ if (!displayName) {
30
+ throw ctx.error("EMPTY_NAME", "displayName cannot be empty");
31
+ }
32
+
33
+ const existing = (await ctx.db.query("Profile", {
34
+ userId: ctx.auth.userId,
35
+ })) as any[];
36
+
37
+ if (existing.length === 0) {
38
+ // New profile — handle must be unique
39
+ const dup = (await ctx.db.query("Profile", { handle })) as any[];
40
+ if (dup.length > 0) {
41
+ throw ctx.error("HANDLE_TAKEN", `@${handle} is taken`);
42
+ }
43
+ const id = await ctx.db.insert("Profile", {
44
+ userId: ctx.auth.userId,
45
+ handle,
46
+ displayName,
47
+ bio: args.bio.trim() || null,
48
+ createdAt: new Date().toISOString(),
49
+ });
50
+ return await ctx.db.get("Profile", id);
51
+ }
52
+
53
+ const profile = existing[0];
54
+ // Update — re-check handle uniqueness only if changing
55
+ if (profile.handle !== handle) {
56
+ const dup = ((await ctx.db.query("Profile", { handle })) as any[]).filter(
57
+ (p) => p.id !== profile.id,
58
+ );
59
+ if (dup.length > 0) {
60
+ throw ctx.error("HANDLE_TAKEN", `@${handle} is taken`);
61
+ }
62
+ }
63
+ await ctx.db.update("Profile", profile.id, {
64
+ handle,
65
+ displayName,
66
+ bio: args.bio.trim() || null,
67
+ });
68
+ return await ctx.db.get("Profile", profile.id);
69
+ },
70
+ });
@@ -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
+ }