@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.
- package/bin/create-pylon.js +347 -1156
- package/package.json +4 -3
- package/templates/_root/.env.example +9 -0
- package/templates/_root/README.md +43 -0
- package/templates/backend/b2b/apps/api/functions/archiveProject.ts +15 -0
- package/templates/backend/b2b/apps/api/functions/createOrg.ts +43 -0
- package/templates/backend/b2b/apps/api/functions/createProject.ts +25 -0
- package/templates/backend/b2b/apps/api/functions/inviteMember.ts +49 -0
- package/templates/backend/b2b/apps/api/functions/myOrgs.ts +37 -0
- package/templates/backend/b2b/apps/api/functions/orgMembers.ts +13 -0
- package/templates/backend/b2b/apps/api/functions/orgProjects.ts +18 -0
- package/templates/backend/b2b/apps/api/functions/removeMember.ts +29 -0
- package/templates/backend/b2b/apps/api/functions/setMemberRole.ts +38 -0
- package/templates/backend/b2b/apps/api/package.json +20 -0
- package/templates/backend/b2b/apps/api/schema.ts +171 -0
- package/templates/backend/b2b/apps/api/tsconfig.json +13 -0
- package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
- package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
- package/templates/backend/barebones/apps/api/package.json +20 -0
- package/templates/backend/barebones/apps/api/schema.ts +61 -0
- package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
- package/templates/backend/chat/apps/api/functions/createRoom.ts +32 -0
- package/templates/backend/chat/apps/api/functions/listRooms.ts +15 -0
- package/templates/backend/chat/apps/api/functions/roomMessages.ts +20 -0
- package/templates/backend/chat/apps/api/functions/sendMessage.ts +37 -0
- package/templates/backend/chat/apps/api/package.json +20 -0
- package/templates/backend/chat/apps/api/schema.ts +93 -0
- package/templates/backend/chat/apps/api/tsconfig.json +13 -0
- package/templates/backend/consumer/apps/api/functions/createPost.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/deletePost.ts +21 -0
- package/templates/backend/consumer/apps/api/functions/feed.ts +57 -0
- package/templates/backend/consumer/apps/api/functions/myProfile.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/profilePosts.ts +17 -0
- package/templates/backend/consumer/apps/api/functions/toggleLike.ts +48 -0
- package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +70 -0
- package/templates/backend/consumer/apps/api/package.json +20 -0
- package/templates/backend/consumer/apps/api/schema.ts +130 -0
- package/templates/backend/consumer/apps/api/tsconfig.json +13 -0
- package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
- package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
- package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
- package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
- package/templates/backend/todo/apps/api/package.json +20 -0
- package/templates/backend/todo/apps/api/schema.ts +85 -0
- package/templates/backend/todo/apps/api/tsconfig.json +13 -0
- package/templates/expo/barebones/apps/expo/App.tsx +166 -0
- package/templates/expo/barebones/apps/expo/app.json +31 -0
- package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
- package/templates/expo/barebones/apps/expo/package.json +30 -0
- package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
- package/templates/expo/chat/apps/expo/App.tsx +414 -0
- package/templates/expo/chat/apps/expo/app.json +25 -0
- package/templates/expo/chat/apps/expo/babel.config.js +6 -0
- package/templates/expo/chat/apps/expo/package.json +30 -0
- package/templates/expo/chat/apps/expo/tsconfig.json +16 -0
- package/templates/expo/consumer/apps/expo/App.tsx +360 -0
- package/templates/expo/consumer/apps/expo/app.json +25 -0
- package/templates/expo/consumer/apps/expo/babel.config.js +6 -0
- package/templates/expo/consumer/apps/expo/package.json +30 -0
- package/templates/expo/consumer/apps/expo/tsconfig.json +16 -0
- package/templates/expo/todo/apps/expo/App.tsx +287 -0
- package/templates/expo/todo/apps/expo/app.json +25 -0
- package/templates/expo/todo/apps/expo/babel.config.js +6 -0
- package/templates/expo/todo/apps/expo/package.json +30 -0
- package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
- package/templates/ios/barebones/apps/ios/Package.swift +34 -0
- package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
- package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/ios/barebones/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
- package/templates/ios/barebones/apps/ios/project.yml +42 -0
- package/templates/ios/chat/apps/ios/Package.swift +34 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +120 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
- package/templates/ios/chat/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +35 -0
- package/templates/ios/chat/apps/ios/project.yml +42 -0
- package/templates/ios/consumer/apps/ios/Package.swift +23 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
- package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +29 -0
- package/templates/ios/consumer/apps/ios/project.yml +42 -0
- package/templates/ios/todo/apps/ios/Package.swift +23 -0
- package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
- package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
- package/templates/ios/todo/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
- package/templates/ios/todo/apps/ios/project.yml +32 -0
- package/templates/mac/b2b/apps/mac/Package.swift +22 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +15 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/OrgPickerView.swift +178 -0
- package/templates/mac/b2b/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/b2b/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/b2b/apps/mac/project.yml +34 -0
- package/templates/mac/barebones/apps/mac/Package.swift +33 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/ContentView.swift +104 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/mac/barebones/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/barebones/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/barebones/apps/mac/project.yml +34 -0
- package/templates/mac/chat/apps/mac/Package.swift +33 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/ChatRootView.swift +140 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +26 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/RoomView.swift +137 -0
- package/templates/mac/chat/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +37 -0
- package/templates/mac/chat/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/chat/apps/mac/project.yml +34 -0
- package/templates/mac/consumer/apps/mac/Package.swift +33 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/FeedView.swift +170 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +42 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +60 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/RootView.swift +30 -0
- package/templates/mac/consumer/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +31 -0
- package/templates/mac/consumer/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/consumer/apps/mac/project.yml +34 -0
- package/templates/mac/todo/apps/mac/Package.swift +33 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/Models.swift +19 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/TodoListView.swift +244 -0
- package/templates/mac/todo/apps/mac/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +30 -0
- package/templates/mac/todo/apps/mac/__APP_NAME_PASCAL__.entitlements +13 -0
- package/templates/mac/todo/apps/mac/project.yml +34 -0
- package/templates/ui/packages/ui/package.json +26 -0
- package/templates/ui/packages/ui/src/button.tsx +44 -0
- package/templates/ui/packages/ui/src/card.tsx +39 -0
- package/templates/ui/packages/ui/src/cn.ts +12 -0
- package/templates/ui/packages/ui/src/index.ts +4 -0
- package/templates/ui/packages/ui/src/input.tsx +19 -0
- package/templates/ui/packages/ui/tsconfig.json +15 -0
- package/templates/web/b2b/apps/web/next-env.d.ts +2 -0
- package/templates/web/b2b/apps/web/next.config.ts +24 -0
- package/templates/web/b2b/apps/web/package.json +29 -0
- package/templates/web/b2b/apps/web/postcss.config.mjs +3 -0
- package/templates/web/b2b/apps/web/src/app/components/OrgPicker.tsx +171 -0
- package/templates/web/b2b/apps/web/src/app/globals.css +6 -0
- package/templates/web/b2b/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/b2b/apps/web/src/app/page.tsx +39 -0
- package/templates/web/b2b/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/b2b/apps/web/tsconfig.json +26 -0
- package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
- package/templates/web/barebones/apps/web/next.config.ts +40 -0
- package/templates/web/barebones/apps/web/package.json +29 -0
- package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
- package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
- package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
- package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
- package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
- package/templates/web/barebones/apps/web/tsconfig.json +26 -0
- package/templates/web/chat/apps/web/next-env.d.ts +2 -0
- package/templates/web/chat/apps/web/next.config.ts +24 -0
- package/templates/web/chat/apps/web/package.json +29 -0
- package/templates/web/chat/apps/web/postcss.config.mjs +3 -0
- package/templates/web/chat/apps/web/src/app/components/ChatRoom.tsx +250 -0
- package/templates/web/chat/apps/web/src/app/globals.css +6 -0
- package/templates/web/chat/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/chat/apps/web/src/app/page.tsx +51 -0
- package/templates/web/chat/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/chat/apps/web/tsconfig.json +26 -0
- package/templates/web/consumer/apps/web/next-env.d.ts +2 -0
- package/templates/web/consumer/apps/web/next.config.ts +24 -0
- package/templates/web/consumer/apps/web/package.json +29 -0
- package/templates/web/consumer/apps/web/postcss.config.mjs +3 -0
- package/templates/web/consumer/apps/web/src/app/components/Feed.tsx +295 -0
- package/templates/web/consumer/apps/web/src/app/globals.css +6 -0
- package/templates/web/consumer/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/consumer/apps/web/src/app/page.tsx +55 -0
- package/templates/web/consumer/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/consumer/apps/web/tsconfig.json +26 -0
- package/templates/web/todo/apps/web/next-env.d.ts +2 -0
- package/templates/web/todo/apps/web/next.config.ts +24 -0
- package/templates/web/todo/apps/web/package.json +32 -0
- package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
- package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
- package/templates/web/todo/apps/web/src/app/globals.css +6 -0
- package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
- package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
- 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,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
|
+
}
|