create-noy-db 0.1.0-pre.4
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 +21 -0
- package/README.md +283 -0
- package/dist/bin/create.d.ts +1 -0
- package/dist/bin/create.js +815 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/bin/noy-db.d.ts +1 -0
- package/dist/bin/noy-db.js +555 -0
- package/dist/bin/noy-db.js.map +1 -0
- package/dist/index.d.ts +697 -0
- package/dist/index.js +967 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/templates/electron/README.md +85 -0
- package/templates/electron/_gitignore +31 -0
- package/templates/electron/electron/main.ts +51 -0
- package/templates/electron/index.html +12 -0
- package/templates/electron/package.json +32 -0
- package/templates/electron/src/App.vue +75 -0
- package/templates/electron/src/components/AddInvoiceForm.vue +39 -0
- package/templates/electron/src/components/InvoiceTable.vue +41 -0
- package/templates/electron/src/main.ts +48 -0
- package/templates/electron/src/stores/invoices.ts +35 -0
- package/templates/electron/src/style.css +119 -0
- package/templates/electron/tsconfig.json +19 -0
- package/templates/electron/vite.config.ts +21 -0
- package/templates/nuxt-default/README.md +38 -0
- package/templates/nuxt-default/_gitignore +32 -0
- package/templates/nuxt-default/app/app.vue +37 -0
- package/templates/nuxt-default/app/pages/index.vue +21 -0
- package/templates/nuxt-default/app/pages/invoices.vue +62 -0
- package/templates/nuxt-default/app/stores/invoices.ts +23 -0
- package/templates/nuxt-default/nuxt.config.ts +30 -0
- package/templates/nuxt-default/package.json +28 -0
- package/templates/nuxt-default/tsconfig.json +3 -0
- package/templates/vanilla/README.md +77 -0
- package/templates/vanilla/_gitignore +29 -0
- package/templates/vanilla/index.html +54 -0
- package/templates/vanilla/package.json +21 -0
- package/templates/vanilla/src/main.ts +153 -0
- package/templates/vanilla/src/style.css +127 -0
- package/templates/vanilla/tsconfig.json +17 -0
- package/templates/vanilla/vite.config.ts +10 -0
- package/templates/vite-vue/README.md +95 -0
- package/templates/vite-vue/_gitignore +29 -0
- package/templates/vite-vue/index.html +12 -0
- package/templates/vite-vue/package.json +28 -0
- package/templates/vite-vue/src/App.vue +75 -0
- package/templates/vite-vue/src/components/AddInvoiceForm.vue +39 -0
- package/templates/vite-vue/src/components/InvoiceTable.vue +41 -0
- package/templates/vite-vue/src/main.ts +55 -0
- package/templates/vite-vue/src/stores/invoices.ts +35 -0
- package/templates/vite-vue/src/style.css +119 -0
- package/templates/vite-vue/tsconfig.json +18 -0
- package/templates/vite-vue/vite.config.ts +12 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{PROJECT_NAME}} — noy-db vanilla</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app">
|
|
11
|
+
<header>
|
|
12
|
+
<h1>{{PROJECT_NAME}}</h1>
|
|
13
|
+
<p class="subtitle">
|
|
14
|
+
A zero-knowledge encrypted invoice store. Open DevTools →
|
|
15
|
+
Application → IndexedDB to see the ciphertext.
|
|
16
|
+
</p>
|
|
17
|
+
</header>
|
|
18
|
+
|
|
19
|
+
<section id="status" class="status">
|
|
20
|
+
<p class="status-text">Loading…</p>
|
|
21
|
+
</section>
|
|
22
|
+
|
|
23
|
+
<section id="controls" class="controls" hidden>
|
|
24
|
+
<button id="add-invoice">Add invoice</button>
|
|
25
|
+
<button id="refresh">Refresh</button>
|
|
26
|
+
<button id="close-vault" class="danger">Close vault</button>
|
|
27
|
+
</section>
|
|
28
|
+
|
|
29
|
+
<section id="invoices" class="invoices" hidden>
|
|
30
|
+
<h2>Invoices</h2>
|
|
31
|
+
<table>
|
|
32
|
+
<thead>
|
|
33
|
+
<tr>
|
|
34
|
+
<th>ID</th>
|
|
35
|
+
<th>Client</th>
|
|
36
|
+
<th>Amount</th>
|
|
37
|
+
<th>Status</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody id="invoices-body"></tbody>
|
|
41
|
+
</table>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
44
|
+
<footer>
|
|
45
|
+
<p>
|
|
46
|
+
Encrypted with AES-256-GCM. Key derived from your passphrase via
|
|
47
|
+
PBKDF2-SHA256 (600 000 iterations). The only people who can read
|
|
48
|
+
your data are those who hold the passphrase.
|
|
49
|
+
</p>
|
|
50
|
+
</footer>
|
|
51
|
+
</div>
|
|
52
|
+
<script type="module" src="/src/main.ts"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"verify": "noy-db verify"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@noy-db/hub": "^0.12.0",
|
|
14
|
+
"@noy-db/to-browser-idb": "^0.12.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"create-noy-db": "^0.12.0",
|
|
18
|
+
"typescript": "^5.6.0",
|
|
19
|
+
"vite": "^5.4.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{PROJECT_NAME}} — noy-db vanilla starter.
|
|
3
|
+
*
|
|
4
|
+
* A minimal, no-framework integration of noy-db. Everything this file
|
|
5
|
+
* needs to do:
|
|
6
|
+
*
|
|
7
|
+
* 1. Prompt for a passphrase (derives the encryption key).
|
|
8
|
+
* 2. Open an encrypted vault backed by IndexedDB.
|
|
9
|
+
* 3. Render a table of invoices.
|
|
10
|
+
* 4. Let the user add, refresh, and lock.
|
|
11
|
+
*
|
|
12
|
+
* The entire app is ~200 lines — the library does the heavy lifting.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createNoydb, type Noydb, type Vault, type Collection } from '@noy-db/hub'
|
|
16
|
+
import { browserIdbStore } from '@noy-db/to-browser-idb'
|
|
17
|
+
import './style.css'
|
|
18
|
+
|
|
19
|
+
// ─── Domain type ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface Invoice {
|
|
22
|
+
id: string
|
|
23
|
+
client: string
|
|
24
|
+
amount: number
|
|
25
|
+
status: 'draft' | 'open' | 'paid' | 'overdue'
|
|
26
|
+
issueDate: string // ISO-8601
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── DOM references ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const statusEl = document.querySelector<HTMLElement>('#status')!
|
|
32
|
+
const statusTextEl = document.querySelector<HTMLElement>('.status-text')!
|
|
33
|
+
const controlsEl = document.querySelector<HTMLElement>('#controls')!
|
|
34
|
+
const invoicesSection = document.querySelector<HTMLElement>('#invoices')!
|
|
35
|
+
const invoicesBody = document.querySelector<HTMLTableSectionElement>('#invoices-body')!
|
|
36
|
+
|
|
37
|
+
const addBtn = document.querySelector<HTMLButtonElement>('#add-invoice')!
|
|
38
|
+
const refreshBtn = document.querySelector<HTMLButtonElement>('#refresh')!
|
|
39
|
+
const closeBtn = document.querySelector<HTMLButtonElement>('#close-vault')!
|
|
40
|
+
|
|
41
|
+
// ─── App state ─────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
let db: Noydb | null = null
|
|
44
|
+
let vault: Vault | null = null
|
|
45
|
+
let invoices: Collection<Invoice> | null = null
|
|
46
|
+
|
|
47
|
+
// ─── Lifecycle ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
async function unlock() {
|
|
50
|
+
// In a real app you would build a proper modal. For the starter we
|
|
51
|
+
// use the browser prompt — it blocks, it's ugly, and it gets the
|
|
52
|
+
// passphrase in two lines.
|
|
53
|
+
const passphrase = prompt(
|
|
54
|
+
'Enter passphrase for {{PROJECT_NAME}}\n\n' +
|
|
55
|
+
'This derives the master encryption key. Same passphrase every time.\n' +
|
|
56
|
+
'Lose it and the data is unrecoverable (by design).',
|
|
57
|
+
)
|
|
58
|
+
if (!passphrase) {
|
|
59
|
+
showStatus('Cancelled — reload to try again.')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
showStatus('Unlocking vault…')
|
|
64
|
+
|
|
65
|
+
db = await createNoydb({
|
|
66
|
+
store: browserIdbStore({ prefix: '{{PROJECT_NAME}}' }),
|
|
67
|
+
user: 'owner',
|
|
68
|
+
secret: passphrase,
|
|
69
|
+
})
|
|
70
|
+
vault = await db.openVault('demo')
|
|
71
|
+
invoices = vault.collection<Invoice>('invoices')
|
|
72
|
+
|
|
73
|
+
statusEl.hidden = true
|
|
74
|
+
controlsEl.hidden = false
|
|
75
|
+
invoicesSection.hidden = false
|
|
76
|
+
await render()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function render() {
|
|
80
|
+
if (!invoices) return
|
|
81
|
+
const rows = await invoices.list()
|
|
82
|
+
rows.sort((a, b) => a.issueDate.localeCompare(b.issueDate))
|
|
83
|
+
|
|
84
|
+
invoicesBody.innerHTML = ''
|
|
85
|
+
if (rows.length === 0) {
|
|
86
|
+
const tr = document.createElement('tr')
|
|
87
|
+
tr.innerHTML = `<td colspan="4" class="empty">No invoices yet — click "Add invoice" to create one.</td>`
|
|
88
|
+
invoicesBody.appendChild(tr)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
for (const inv of rows) {
|
|
92
|
+
const tr = document.createElement('tr')
|
|
93
|
+
tr.innerHTML = `
|
|
94
|
+
<td>${inv.id}</td>
|
|
95
|
+
<td>${escapeHtml(inv.client)}</td>
|
|
96
|
+
<td>${inv.amount.toLocaleString()}</td>
|
|
97
|
+
<td><span class="status-pill status-${inv.status}">${inv.status}</span></td>
|
|
98
|
+
`
|
|
99
|
+
invoicesBody.appendChild(tr)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function addInvoice() {
|
|
104
|
+
if (!invoices) return
|
|
105
|
+
const client = prompt('Client name?') || 'Unnamed'
|
|
106
|
+
const amountStr = prompt('Amount?') || '0'
|
|
107
|
+
const amount = Number.parseFloat(amountStr)
|
|
108
|
+
if (Number.isNaN(amount)) {
|
|
109
|
+
alert('Amount must be a number.')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
const id = `inv-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
|
113
|
+
await invoices.put(id, {
|
|
114
|
+
id,
|
|
115
|
+
client,
|
|
116
|
+
amount,
|
|
117
|
+
status: 'draft',
|
|
118
|
+
issueDate: new Date().toISOString().slice(0, 10),
|
|
119
|
+
})
|
|
120
|
+
await render()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function closeVault() {
|
|
124
|
+
if (!db) return
|
|
125
|
+
await db.close()
|
|
126
|
+
db = null
|
|
127
|
+
vault = null
|
|
128
|
+
invoices = null
|
|
129
|
+
controlsEl.hidden = true
|
|
130
|
+
invoicesSection.hidden = true
|
|
131
|
+
showStatus('Vault closed. Reload the page to unlock again.')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Helpers ───────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function showStatus(text: string) {
|
|
137
|
+
statusEl.hidden = false
|
|
138
|
+
statusTextEl.textContent = text
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function escapeHtml(s: string): string {
|
|
142
|
+
const div = document.createElement('div')
|
|
143
|
+
div.textContent = s
|
|
144
|
+
return div.innerHTML
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Wire everything up ────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
addBtn.addEventListener('click', () => void addInvoice())
|
|
150
|
+
refreshBtn.addEventListener('click', () => void render())
|
|
151
|
+
closeBtn.addEventListener('click', () => void closeVault())
|
|
152
|
+
|
|
153
|
+
window.addEventListener('DOMContentLoaded', () => void unlock())
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
4
|
+
color: #1f2937;
|
|
5
|
+
background: #f9fafb;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 1rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#app {
|
|
18
|
+
max-width: 900px;
|
|
19
|
+
margin: 0 auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
header {
|
|
23
|
+
border-bottom: 1px solid #e5e7eb;
|
|
24
|
+
padding-bottom: 1rem;
|
|
25
|
+
margin-bottom: 1.5rem;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1 {
|
|
29
|
+
font-size: 1.5rem;
|
|
30
|
+
margin: 0 0 0.5rem 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.subtitle {
|
|
34
|
+
color: #6b7280;
|
|
35
|
+
font-size: 0.9rem;
|
|
36
|
+
margin: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.status {
|
|
40
|
+
background: white;
|
|
41
|
+
border: 1px solid #e5e7eb;
|
|
42
|
+
border-radius: 0.5rem;
|
|
43
|
+
padding: 1.5rem;
|
|
44
|
+
text-align: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.controls {
|
|
48
|
+
display: flex;
|
|
49
|
+
gap: 0.5rem;
|
|
50
|
+
margin-bottom: 1rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
button {
|
|
54
|
+
padding: 0.5rem 1rem;
|
|
55
|
+
border: 1px solid #2563eb;
|
|
56
|
+
background: #2563eb;
|
|
57
|
+
color: white;
|
|
58
|
+
border-radius: 0.25rem;
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
font-size: 0.875rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
button:hover {
|
|
64
|
+
background: #1d4ed8;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
button.danger {
|
|
68
|
+
background: #dc2626;
|
|
69
|
+
border-color: #dc2626;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.invoices h2 {
|
|
73
|
+
font-size: 1.1rem;
|
|
74
|
+
margin-top: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
table {
|
|
78
|
+
width: 100%;
|
|
79
|
+
border-collapse: collapse;
|
|
80
|
+
background: white;
|
|
81
|
+
border: 1px solid #e5e7eb;
|
|
82
|
+
border-radius: 0.5rem;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
th, td {
|
|
87
|
+
padding: 0.5rem 0.75rem;
|
|
88
|
+
text-align: left;
|
|
89
|
+
border-bottom: 1px solid #e5e7eb;
|
|
90
|
+
font-size: 0.875rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
th {
|
|
94
|
+
background: #f3f4f6;
|
|
95
|
+
font-weight: 600;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tr:last-child td {
|
|
99
|
+
border-bottom: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.empty {
|
|
103
|
+
color: #6b7280;
|
|
104
|
+
text-align: center;
|
|
105
|
+
font-style: italic;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.status-pill {
|
|
109
|
+
display: inline-block;
|
|
110
|
+
padding: 0.125rem 0.5rem;
|
|
111
|
+
border-radius: 0.25rem;
|
|
112
|
+
font-size: 0.75rem;
|
|
113
|
+
text-transform: uppercase;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
}
|
|
116
|
+
.status-draft { background: #f3f4f6; color: #4b5563; }
|
|
117
|
+
.status-open { background: #dbeafe; color: #1e40af; }
|
|
118
|
+
.status-paid { background: #d1fae5; color: #065f46; }
|
|
119
|
+
.status-overdue { background: #fee2e2; color: #b91c1c; }
|
|
120
|
+
|
|
121
|
+
footer {
|
|
122
|
+
margin-top: 3rem;
|
|
123
|
+
padding-top: 1rem;
|
|
124
|
+
border-top: 1px solid #e5e7eb;
|
|
125
|
+
color: #6b7280;
|
|
126
|
+
font-size: 0.85rem;
|
|
127
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"allowSyntheticDefaultImports": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"useDefineForClassFields": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A Vite + Vue 3 + Pinia + [noy-db](https://github.com/vLannaAi/noy-db)
|
|
4
|
+
starter. Pure client-side SPA — no Nuxt, no SSR. Scaffolded by
|
|
5
|
+
`create-noy-db`.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
- **Vite** — dev server + build
|
|
10
|
+
- **Vue 3** — `<script setup>` + composition API
|
|
11
|
+
- **Pinia** — reactive state management
|
|
12
|
+
- **@noy-db/hub** — zero-knowledge encrypted document store
|
|
13
|
+
- **@noy-db/in-pinia** — `defineNoydbStore` wires a Pinia store to an
|
|
14
|
+
encrypted collection
|
|
15
|
+
- **@noy-db/to-browser-idb** — IndexedDB adapter (atomic CAS, large
|
|
16
|
+
quota)
|
|
17
|
+
|
|
18
|
+
Everything stored is encrypted with AES-256-GCM before it touches
|
|
19
|
+
IndexedDB. The adapter only ever sees ciphertext.
|
|
20
|
+
|
|
21
|
+
## When to pick this template
|
|
22
|
+
|
|
23
|
+
- You want **Vue + Pinia** reactivity without a full Nuxt / SSR setup.
|
|
24
|
+
- You're building a **client-side SPA**, a desktop-wrapped
|
|
25
|
+
(Electron / Tauri) app, or embedding into an existing static host.
|
|
26
|
+
- You want a shorter install graph and faster dev server startup than
|
|
27
|
+
Nuxt provides.
|
|
28
|
+
|
|
29
|
+
If you want SSR + file-based routing, pick the `nuxt-default`
|
|
30
|
+
template instead (`npm create noy-db@latest -- --template nuxt-default`).
|
|
31
|
+
If you want no framework at all, pick `vanilla`.
|
|
32
|
+
|
|
33
|
+
## Getting started
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm install # or npm/yarn/bun
|
|
37
|
+
pnpm dev # vite dev on http://localhost:5173
|
|
38
|
+
pnpm build # production build
|
|
39
|
+
pnpm preview # preview the production build
|
|
40
|
+
pnpm verify # run the noy-db integrity check
|
|
41
|
+
pnpm typecheck # vue-tsc
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
First time the page loads it will prompt for a **passphrase** — that
|
|
45
|
+
passphrase derives the master key. Lose it and the data is
|
|
46
|
+
unrecoverable (by design).
|
|
47
|
+
|
|
48
|
+
## Project layout
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
src/
|
|
52
|
+
main.ts — app bootstrap: pinia + noydb + mount
|
|
53
|
+
App.vue — root component
|
|
54
|
+
components/
|
|
55
|
+
InvoiceTable.vue — reactive table bound to the store
|
|
56
|
+
AddInvoiceForm.vue — form that dispatches store.add()
|
|
57
|
+
stores/
|
|
58
|
+
invoices.ts — defineNoydbStore<Invoice>('invoices', { vault: 'demo' })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The store exposes:
|
|
62
|
+
|
|
63
|
+
| Member | Description |
|
|
64
|
+
|--------|-------------|
|
|
65
|
+
| `items` | reactive array of records |
|
|
66
|
+
| `count` | reactive getter |
|
|
67
|
+
| `$ready` | promise resolved on first hydration |
|
|
68
|
+
| `add(id, record)` | encrypt + persist + update reactive state |
|
|
69
|
+
| `update(id, record)` | upsert (`Collection.put` semantics) |
|
|
70
|
+
| `remove(id)` | delete + update reactive state |
|
|
71
|
+
| `refresh()` | re-hydrate from the adapter |
|
|
72
|
+
| `query()` | chainable query DSL |
|
|
73
|
+
|
|
74
|
+
## Verifying zero-knowledge
|
|
75
|
+
|
|
76
|
+
Open DevTools → Application → IndexedDB → your origin. Every record
|
|
77
|
+
shows `{ _noydb: 1, _v, _ts, _iv, _data }`. The `_data` blob is
|
|
78
|
+
AES-GCM ciphertext — the literal record contents never appear.
|
|
79
|
+
|
|
80
|
+
## Adding a collection
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx noy-db add clients
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Scaffolds `src/stores/clients.ts` with a typed Client interface +
|
|
87
|
+
a `defineNoydbStore` binding. Edit the generated interface to match
|
|
88
|
+
your domain.
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
- [noy-db getting started](https://github.com/vLannaAi/noy-db/blob/main/docs/choose-your-path.md)
|
|
93
|
+
- [Topology matrix](https://github.com/vLannaAi/noy-db/blob/main/docs/topologies.md)
|
|
94
|
+
- [Architecture](https://github.com/vLannaAi/noy-db/blob/main/docs/architecture.md)
|
|
95
|
+
- [Roadmap](https://github.com/vLannaAi/noy-db/blob/main/ROADMAP.md)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Vite / build output
|
|
2
|
+
dist
|
|
3
|
+
.vite
|
|
4
|
+
|
|
5
|
+
# Node
|
|
6
|
+
node_modules
|
|
7
|
+
*.log
|
|
8
|
+
.npm
|
|
9
|
+
|
|
10
|
+
# Env files
|
|
11
|
+
.env
|
|
12
|
+
.env.local
|
|
13
|
+
.env.*.local
|
|
14
|
+
|
|
15
|
+
# OS
|
|
16
|
+
.DS_Store
|
|
17
|
+
Thumbs.db
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/*
|
|
21
|
+
!.vscode/extensions.json
|
|
22
|
+
.idea
|
|
23
|
+
*.suo
|
|
24
|
+
*.ntvs*
|
|
25
|
+
*.njsproj
|
|
26
|
+
*.sln
|
|
27
|
+
|
|
28
|
+
# Test coverage
|
|
29
|
+
coverage
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{PROJECT_NAME}} — noy-db + Vue 3 + Pinia</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script type="module" src="/src/main.ts"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vue-tsc --noEmit && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "vue-tsc --noEmit",
|
|
11
|
+
"verify": "noy-db verify"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@noy-db/hub": "^0.12.0",
|
|
15
|
+
"@noy-db/in-pinia": "^0.12.0",
|
|
16
|
+
"@noy-db/in-vue": "^0.12.0",
|
|
17
|
+
"@noy-db/to-browser-idb": "^0.12.0",
|
|
18
|
+
"pinia": "^2.2.0",
|
|
19
|
+
"vue": "^3.5.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@vitejs/plugin-vue": "^5.1.0",
|
|
23
|
+
"create-noy-db": "^0.12.0",
|
|
24
|
+
"typescript": "^5.6.0",
|
|
25
|
+
"vite": "^5.4.0",
|
|
26
|
+
"vue-tsc": "^2.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
import { useInvoices, DEFAULT_INVOICES, type Invoice } from './stores/invoices'
|
|
4
|
+
import InvoiceTable from './components/InvoiceTable.vue'
|
|
5
|
+
import AddInvoiceForm from './components/AddInvoiceForm.vue'
|
|
6
|
+
|
|
7
|
+
const store = useInvoices()
|
|
8
|
+
const ready = ref(false)
|
|
9
|
+
|
|
10
|
+
onMounted(async () => {
|
|
11
|
+
await store.$ready
|
|
12
|
+
|
|
13
|
+
// Seed the collection on first load so the demo shows something.
|
|
14
|
+
if (store.items.length === 0) {
|
|
15
|
+
for (const inv of DEFAULT_INVOICES) {
|
|
16
|
+
await store.add(inv.id, inv)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
ready.value = true
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
async function onAdd(invoice: Invoice) {
|
|
23
|
+
await store.add(invoice.id, invoice)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function onRemove(id: string) {
|
|
27
|
+
await store.remove(id)
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="app">
|
|
33
|
+
<header>
|
|
34
|
+
<h1>{{PROJECT_NAME}}</h1>
|
|
35
|
+
<p class="subtitle">
|
|
36
|
+
Encrypted invoices backed by noy-db + Vue 3 + Pinia.
|
|
37
|
+
Open DevTools → Application → IndexedDB to see ciphertext only.
|
|
38
|
+
</p>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<main v-if="ready">
|
|
42
|
+
<section class="summary">
|
|
43
|
+
<div class="stat">
|
|
44
|
+
<div class="label">Invoices</div>
|
|
45
|
+
<div class="value">{{ store.count }}</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="stat">
|
|
48
|
+
<div class="label">Total (open)</div>
|
|
49
|
+
<div class="value">
|
|
50
|
+
{{
|
|
51
|
+
store.items
|
|
52
|
+
.filter((i) => i.status === 'open')
|
|
53
|
+
.reduce((n, i) => n + i.amount, 0)
|
|
54
|
+
.toLocaleString()
|
|
55
|
+
}}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<AddInvoiceForm @submit="onAdd" />
|
|
61
|
+
|
|
62
|
+
<InvoiceTable :invoices="store.items" @remove="onRemove" />
|
|
63
|
+
</main>
|
|
64
|
+
|
|
65
|
+
<p v-else class="loading">Unlocking vault…</p>
|
|
66
|
+
|
|
67
|
+
<footer>
|
|
68
|
+
<p>
|
|
69
|
+
AES-256-GCM at rest. Key derived from your passphrase via
|
|
70
|
+
PBKDF2-SHA256 (600 000 iterations). Lose the passphrase and
|
|
71
|
+
the data is unrecoverable — by design.
|
|
72
|
+
</p>
|
|
73
|
+
</footer>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type { Invoice } from '../stores/invoices'
|
|
4
|
+
|
|
5
|
+
const emit = defineEmits<{ (e: 'submit', invoice: Invoice): void }>()
|
|
6
|
+
|
|
7
|
+
const client = ref('')
|
|
8
|
+
const amount = ref<number | null>(null)
|
|
9
|
+
const status = ref<Invoice['status']>('draft')
|
|
10
|
+
|
|
11
|
+
function submit() {
|
|
12
|
+
if (!client.value || amount.value === null || Number.isNaN(amount.value)) return
|
|
13
|
+
const id = `inv-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
|
14
|
+
emit('submit', {
|
|
15
|
+
id,
|
|
16
|
+
client: client.value,
|
|
17
|
+
amount: amount.value,
|
|
18
|
+
status: status.value,
|
|
19
|
+
issueDate: new Date().toISOString().slice(0, 10),
|
|
20
|
+
})
|
|
21
|
+
client.value = ''
|
|
22
|
+
amount.value = null
|
|
23
|
+
status.value = 'draft'
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<form class="add-form" @submit.prevent="submit">
|
|
29
|
+
<input v-model="client" placeholder="Client name" required />
|
|
30
|
+
<input v-model.number="amount" type="number" min="0" step="0.01" placeholder="Amount" required />
|
|
31
|
+
<select v-model="status">
|
|
32
|
+
<option value="draft">draft</option>
|
|
33
|
+
<option value="open">open</option>
|
|
34
|
+
<option value="paid">paid</option>
|
|
35
|
+
<option value="overdue">overdue</option>
|
|
36
|
+
</select>
|
|
37
|
+
<button type="submit">Add invoice</button>
|
|
38
|
+
</form>
|
|
39
|
+
</template>
|