@cellapp/cli 0.1.0 → 0.2.0
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/LICENSE +1 -1
- package/NOTICE +1 -1
- package/README.md +19 -6
- package/dist/packages/cli/src/client.js +1 -1
- package/dist/packages/cli/src/data.js +40 -0
- package/dist/packages/cli/src/data.js.map +1 -0
- package/dist/packages/cli/src/deploy.js +10 -5
- package/dist/packages/cli/src/deploy.js.map +1 -1
- package/dist/packages/cli/src/main.js +8 -4
- package/dist/packages/cli/src/main.js.map +1 -1
- package/dist/packages/shared/src/data.js +103 -0
- package/dist/packages/shared/src/data.js.map +1 -0
- package/package.json +26 -7
- package/skills/cellapp-deploy/SKILL.md +35 -10
- package/skills/cellapp-deploy/examples/notes/README.md +21 -0
- package/skills/cellapp-deploy/examples/notes/app.ts +115 -0
- package/skills/cellapp-deploy/examples/notes/cellapp.data.json +22 -0
- package/skills/cellapp-deploy/examples/notes/index.html +40 -0
- package/skills/cellapp-deploy/examples/notes/notes.ts +37 -0
- package/skills/cellapp-deploy/examples/notes/package-lock.json +622 -0
- package/skills/cellapp-deploy/examples/notes/package.json +16 -0
- package/skills/cellapp-deploy/examples/notes/tsconfig.json +13 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { addNote, currentAppUser, deleteNote, listNotes, updateNote } from './notes.js';
|
|
2
|
+
|
|
3
|
+
const form = document.querySelector<HTMLFormElement>('#create-form')!;
|
|
4
|
+
const input = document.querySelector<HTMLInputElement>('#note-body')!;
|
|
5
|
+
const notes = document.querySelector<HTMLUListElement>('#notes')!;
|
|
6
|
+
const identity = document.querySelector<HTMLElement>('#identity')!;
|
|
7
|
+
const status = document.querySelector<HTMLElement>('#status')!;
|
|
8
|
+
const check = document.querySelector<HTMLButtonElement>('#crud')!;
|
|
9
|
+
|
|
10
|
+
function report(message: string, error = false): void {
|
|
11
|
+
status.textContent = message;
|
|
12
|
+
status.classList.toggle('error', error);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function failure(error: unknown): void {
|
|
16
|
+
report(error instanceof Error ? error.message : String(error), true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function refresh(): Promise<void> {
|
|
20
|
+
const rows = await listNotes();
|
|
21
|
+
notes.replaceChildren();
|
|
22
|
+
if (!rows?.length) {
|
|
23
|
+
const empty = document.createElement('li');
|
|
24
|
+
empty.textContent = '还没有笔记。';
|
|
25
|
+
notes.append(empty);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const item = document.createElement('li');
|
|
30
|
+
const body = document.createElement('span');
|
|
31
|
+
body.textContent = row.body;
|
|
32
|
+
const actions = document.createElement('div');
|
|
33
|
+
const edit = document.createElement('button');
|
|
34
|
+
edit.type = 'button';
|
|
35
|
+
edit.className = 'secondary';
|
|
36
|
+
edit.textContent = '编辑';
|
|
37
|
+
edit.setAttribute('aria-label', `编辑笔记 ${row.body}`);
|
|
38
|
+
edit.addEventListener('click', async () => {
|
|
39
|
+
const next = prompt('修改笔记', row.body);
|
|
40
|
+
if (next === null || !next.trim()) return;
|
|
41
|
+
edit.disabled = true;
|
|
42
|
+
try {
|
|
43
|
+
await updateNote(row.id, next.trim());
|
|
44
|
+
await refresh();
|
|
45
|
+
report('笔记已更新');
|
|
46
|
+
} catch (error) { failure(error); }
|
|
47
|
+
finally { edit.disabled = false; }
|
|
48
|
+
});
|
|
49
|
+
const remove = document.createElement('button');
|
|
50
|
+
remove.type = 'button';
|
|
51
|
+
remove.className = 'secondary';
|
|
52
|
+
remove.textContent = '删除';
|
|
53
|
+
remove.setAttribute('aria-label', `删除笔记 ${row.body}`);
|
|
54
|
+
remove.addEventListener('click', async () => {
|
|
55
|
+
remove.disabled = true;
|
|
56
|
+
try {
|
|
57
|
+
await deleteNote(row.id);
|
|
58
|
+
await refresh();
|
|
59
|
+
report('笔记已删除');
|
|
60
|
+
} catch (error) { failure(error); }
|
|
61
|
+
finally { remove.disabled = false; }
|
|
62
|
+
});
|
|
63
|
+
actions.append(edit, remove);
|
|
64
|
+
item.append(body, actions);
|
|
65
|
+
notes.append(item);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
form.addEventListener('submit', async event => {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
const body = input.value.trim();
|
|
72
|
+
if (!body) return;
|
|
73
|
+
const save = form.querySelector<HTMLButtonElement>('button')!;
|
|
74
|
+
save.disabled = true;
|
|
75
|
+
try {
|
|
76
|
+
await addNote(body);
|
|
77
|
+
input.value = '';
|
|
78
|
+
await refresh();
|
|
79
|
+
report('笔记已保存');
|
|
80
|
+
} catch (error) { failure(error); }
|
|
81
|
+
finally { save.disabled = false; }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
check.addEventListener('click', async () => {
|
|
85
|
+
check.disabled = true;
|
|
86
|
+
const marker = `CRUD ${crypto.randomUUID()}`;
|
|
87
|
+
try {
|
|
88
|
+
const inserted = await addNote(marker);
|
|
89
|
+
if (inserted?.length !== 1) throw new Error('创建后未返回笔记');
|
|
90
|
+
const row = inserted[0]!;
|
|
91
|
+
const selected = await listNotes();
|
|
92
|
+
if (!selected?.some(note => note.id === row.id && note.body === marker)) throw new Error('未读到刚创建的笔记');
|
|
93
|
+
const changed = await updateNote(row.id, marker + ' updated');
|
|
94
|
+
if (changed?.[0]?.body !== marker + ' updated') throw new Error('更新后的内容不一致');
|
|
95
|
+
await deleteNote(row.id);
|
|
96
|
+
const remaining = await listNotes();
|
|
97
|
+
if (remaining?.some(note => note.id === row.id)) throw new Error('删除后仍能读到笔记');
|
|
98
|
+
await refresh();
|
|
99
|
+
report('CRUD passed');
|
|
100
|
+
} catch (error) { failure(error); }
|
|
101
|
+
finally { check.disabled = false; }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
void (async () => {
|
|
105
|
+
try {
|
|
106
|
+
const user = await currentAppUser();
|
|
107
|
+
document.body.dataset.user = user;
|
|
108
|
+
identity.textContent = `当前应用用户:${user.slice(0, 8)}…`;
|
|
109
|
+
await refresh();
|
|
110
|
+
report('应用数据库已连接');
|
|
111
|
+
} catch (error) {
|
|
112
|
+
identity.textContent = '无法连接应用数据库';
|
|
113
|
+
failure(error);
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"tables": [
|
|
4
|
+
{
|
|
5
|
+
"name": "notes",
|
|
6
|
+
"columns": [
|
|
7
|
+
{"name": "id", "type": "uuid"},
|
|
8
|
+
{"name": "owner_id", "type": "text"},
|
|
9
|
+
{"name": "body", "type": "text"}
|
|
10
|
+
],
|
|
11
|
+
"primaryKey": ["id"],
|
|
12
|
+
"indexes": [{"columns": ["owner_id"]}],
|
|
13
|
+
"policies": {
|
|
14
|
+
"select": "own-rows",
|
|
15
|
+
"insert": "own-rows",
|
|
16
|
+
"update": "own-rows",
|
|
17
|
+
"delete": "own-rows",
|
|
18
|
+
"ownerColumn": "owner_id"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Cellapp 笔记 Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root { font-family: system-ui, sans-serif; color: #1c2b25; background: #f4f7f3; }
|
|
9
|
+
body { max-width: 720px; margin: 40px auto; padding: 0 20px; }
|
|
10
|
+
main { background: white; padding: 28px; border: 1px solid #d9e5dc; border-radius: 16px; box-shadow: 0 8px 32px #1536230d; }
|
|
11
|
+
h1 { margin: 0 0 8px; }
|
|
12
|
+
p { line-height: 1.5; }
|
|
13
|
+
form { display: flex; gap: 8px; margin: 20px 0; }
|
|
14
|
+
input { flex: 1; min-width: 0; padding: 10px; border: 1px solid #adbfaf; border-radius: 8px; font: inherit; }
|
|
15
|
+
button { padding: 9px 14px; border: 0; border-radius: 8px; color: white; background: #21684a; font: inherit; cursor: pointer; }
|
|
16
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
17
|
+
button.secondary { color: #21684a; background: #e5f0e8; }
|
|
18
|
+
li { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-top: 1px solid #e7eee7; }
|
|
19
|
+
li span { overflow-wrap: anywhere; }
|
|
20
|
+
li div { display: flex; gap: 8px; flex: none; }
|
|
21
|
+
#status { min-height: 1.5em; color: #345446; }
|
|
22
|
+
#status.error { color: #a22828; }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<main>
|
|
27
|
+
<h1>Notes app</h1>
|
|
28
|
+
<p>使用 Cellapp 登录身份保存笔记。每位用户只能读写自己的记录。</p>
|
|
29
|
+
<p id="identity">正在连接应用数据库…</p>
|
|
30
|
+
<form id="create-form">
|
|
31
|
+
<input id="note-body" aria-label="笔记内容" maxlength="2000" required placeholder="写一条笔记">
|
|
32
|
+
<button type="submit">保存笔记</button>
|
|
33
|
+
</form>
|
|
34
|
+
<p id="status" role="status" aria-live="polite"></p>
|
|
35
|
+
<ul id="notes"></ul>
|
|
36
|
+
<button id="crud" class="secondary" type="button">Run CRUD</button>
|
|
37
|
+
</main>
|
|
38
|
+
<script type="module" src="/app.js"></script>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
const client = createClient(window.location.origin, 'cellapp-public', {
|
|
4
|
+
auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false },
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export async function currentAppUser(): Promise<string> {
|
|
8
|
+
const response = await fetch('/_cellapp/me', { credentials: 'same-origin' });
|
|
9
|
+
if (!response.ok) throw new Error(response.status === 401 ? 'Sign in to Cellapp' : 'Application access required');
|
|
10
|
+
const value: unknown = await response.json();
|
|
11
|
+
if (!value || typeof value !== 'object' || !('id' in value) || typeof value.id !== 'string') throw new Error('Invalid application identity');
|
|
12
|
+
return value.id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function addNote(body: string) {
|
|
16
|
+
const ownerId = await currentAppUser();
|
|
17
|
+
const result = await client.from('notes').insert({ id: crypto.randomUUID(), owner_id: ownerId, body }).select('id,body');
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
return result.data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function listNotes() {
|
|
23
|
+
const result = await client.from('notes').select('id,body').order('id').range(0, 49);
|
|
24
|
+
if (result.error) throw result.error;
|
|
25
|
+
return result.data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function updateNote(id: string, body: string) {
|
|
29
|
+
const result = await client.from('notes').update({ body }).eq('id', id).select('id,body');
|
|
30
|
+
if (result.error) throw result.error;
|
|
31
|
+
return result.data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function deleteNote(id: string) {
|
|
35
|
+
const result = await client.from('notes').delete().eq('id', id);
|
|
36
|
+
if (result.error) throw result.error;
|
|
37
|
+
}
|