@voxgig/build 4.1.0 → 4.3.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.
Files changed (56) hide show
  1. package/build.ts +23 -0
  2. package/dist/build.d.ts +34 -1
  3. package/dist/build.js +19 -1
  4. package/dist/build.js.map +1 -1
  5. package/dist/env/env_gen.d.ts +17 -0
  6. package/dist/env/env_gen.js +154 -0
  7. package/dist/env/env_gen.js.map +1 -0
  8. package/dist/env/lambda/generate.d.ts +3 -2
  9. package/dist/env/lambda/generate.js +13 -9
  10. package/dist/env/lambda/generate.js.map +1 -1
  11. package/dist/env/web/web_gen.d.ts +19 -0
  12. package/dist/env/web/web_gen.js +93 -0
  13. package/dist/env/web/web_gen.js.map +1 -0
  14. package/env/env_gen.ts +179 -0
  15. package/env/lambda/generate.ts +15 -8
  16. package/env/web/web_gen.ts +115 -0
  17. package/package.json +1 -1
  18. package/tm/env/aws/README.md.frag +9 -0
  19. package/tm/env/aws/lambda.entry.ts.frag +89 -0
  20. package/tm/env/aws/serverless.yml.frag +23 -0
  21. package/tm/env/azure/README.md.frag +7 -0
  22. package/tm/env/azure/entry.ts.frag +49 -0
  23. package/tm/env/azure/host.json.frag +8 -0
  24. package/tm/env/basic/README.md.frag +7 -0
  25. package/tm/env/basic/backend.service.frag +14 -0
  26. package/tm/env/basic/deploy.sh.frag +13 -0
  27. package/tm/env/cloudflare/README.md.frag +7 -0
  28. package/tm/env/cloudflare/worker.ts.frag +13 -0
  29. package/tm/env/cloudflare/wrangler.toml.frag +7 -0
  30. package/tm/env/docker/Dockerfile.frag +10 -0
  31. package/tm/env/docker/README.md.frag +7 -0
  32. package/tm/env/docker/compose.yml.frag +10 -0
  33. package/tm/env/local/README.md.frag +7 -0
  34. package/tm/env/local/run.sh.frag +6 -0
  35. package/tm/env/server.entry.ts.frag +48 -0
  36. package/tm/env/vm/README.md.frag +5 -0
  37. package/tm/env/vm/cloud-init.yaml.frag +23 -0
  38. package/tm/web/backend/env/web/web.ts.frag +120 -0
  39. package/tm/web/backend/srv/auth/auth-srv.ts.frag +4 -0
  40. package/tm/web/backend/srv/auth/get_info.ts.frag +5 -0
  41. package/tm/web/backend/srv/auth/load_auth.ts.frag +6 -0
  42. package/tm/web/backend/srv/auth/signin_user.ts.frag +14 -0
  43. package/tm/web/backend/srv/auth/signout_user.ts.frag +5 -0
  44. package/tm/web/backend/srv/auth/web_load_auth.ts.frag +12 -0
  45. package/tm/web/backend/srv/auth/web_signin_user.ts.frag +25 -0
  46. package/tm/web/backend/srv/auth/web_signout_user.ts.frag +9 -0
  47. package/tm/web/web/e2e/smoke.spec.js.frag +21 -0
  48. package/tm/web/web/index.html.frag +13 -0
  49. package/tm/web/web/package.json.frag +17 -0
  50. package/tm/web/web/playwright.config.js.frag +36 -0
  51. package/tm/web/web/src/bus.js.frag +61 -0
  52. package/tm/web/web/src/cmp/admin.js.frag +254 -0
  53. package/tm/web/web/src/cmp/app.js.frag +30 -0
  54. package/tm/web/web/src/cmp/auth.js.frag +97 -0
  55. package/tm/web/web/src/main.js.frag +10 -0
  56. package/tm/web/web/src/style.css.frag +26 -0
@@ -0,0 +1,254 @@
1
+ // Entity admin: a django-admin-style generic CRUD UI, driven entirely by
2
+ // the compiled model (fetched from /model.json). The entity_admin plugin
3
+ // owns the model + per-entity message mapping (state); the
4
+ // <vg-entity-admin> component renders nav / list / form and talks to the
5
+ // plugin (and through it, the backend) only via bus messages.
6
+
7
+ import { bus, emit } from '../bus.js'
8
+
9
+
10
+ // Fields managed by the system, not the form.
11
+ const SYS_FIELDS = ['id', 'owner_id', 't_c', 't_m', 't_mh', 't_ch']
12
+
13
+
14
+ bus.use(function entity_admin() {
15
+ const seneca = this
16
+
17
+ let model = null
18
+
19
+ // Entities from the model: [{ zone, name, canon, fields }]
20
+ function entities() {
21
+ const out = []
22
+ const ent = (model && model.main.ent) || {}
23
+ for (const zone of Object.keys(ent)) {
24
+ if ('sys' === zone) {
25
+ continue
26
+ }
27
+ for (const name of Object.keys(ent[zone])) {
28
+ out.push({
29
+ zone,
30
+ name,
31
+ canon: zone + '/' + name,
32
+ fields: ent[zone][name].field || {},
33
+ })
34
+ }
35
+ }
36
+ return out
37
+ }
38
+
39
+ // Message mapping convention: entity zone/name -> aim:<zone>,<verb>:<name>.
40
+ function msgFor(canon, verb) {
41
+ const [zone, name] = canon.split('/')
42
+ return { aim: zone, [verb]: name }
43
+ }
44
+
45
+ seneca.add('cmp:entity_admin,load:model', function (msg, reply) {
46
+ if (model) {
47
+ return reply({ ok: true, entities: entities() })
48
+ }
49
+ fetch('/model.json').then((r) => r.json()).then((m) => {
50
+ model = m
51
+ reply({ ok: true, entities: entities() })
52
+ }).catch((e) => reply({ ok: false, why: String(e) }))
53
+ })
54
+
55
+ seneca.add('cmp:entity_admin,list:item', function (msg, reply) {
56
+ this.act(msgFor(msg.canon, 'list'), function (err, out) {
57
+ reply(err ? { ok: false, why: err.message } : out)
58
+ })
59
+ })
60
+
61
+ seneca.add('cmp:entity_admin,save:item', function (msg, reply) {
62
+ this.act({ ...msgFor(msg.canon, 'save'), item: msg.item },
63
+ function (err, out) {
64
+ if (!err && out.ok) {
65
+ emit('entity', { canon: msg.canon })
66
+ }
67
+ reply(err ? { ok: false, why: err.message } : out)
68
+ })
69
+ })
70
+
71
+ seneca.add('cmp:entity_admin,remove:item', function (msg, reply) {
72
+ this.act({ ...msgFor(msg.canon, 'remove'), id: msg.id },
73
+ function (err, out) {
74
+ if (!err && out.ok) {
75
+ emit('entity', { canon: msg.canon })
76
+ }
77
+ reply(err ? { ok: false, why: err.message } : out)
78
+ })
79
+ })
80
+ })
81
+
82
+
83
+ function inputFor(fname, fdef, value) {
84
+ const kind = (fdef && fdef.kind) || 'String'
85
+ const val = null == value ? '' : value
86
+ if ('Boolean' === kind) {
87
+ return `<input name="${fname}" type="checkbox" ${true === value ? 'checked' : ''} />`
88
+ }
89
+ if ('Number' === kind) {
90
+ return `<input name="${fname}" type="number" value="${val}" />`
91
+ }
92
+ return `<input name="${fname}" type="text" value="${String(val).replace(/"/g, '&quot;')}" />`
93
+ }
94
+
95
+
96
+ class VgEntityAdmin extends HTMLElement {
97
+ constructor() {
98
+ super()
99
+ this.canon = null
100
+ this.editing = null // item being edited, or {} for new
101
+ }
102
+
103
+ connectedCallback() {
104
+ this.render()
105
+ }
106
+
107
+ async render() {
108
+ const res = await bus.post('cmp:entity_admin,load:model')
109
+ if (!res.ok) {
110
+ this.innerHTML = `<p>model load failed: ${res.why}</p>`
111
+ return
112
+ }
113
+ this.entities = res.entities
114
+ if (null == this.canon && 0 < res.entities.length) {
115
+ this.canon = res.entities[0].canon
116
+ }
117
+
118
+ const nav = res.entities.map((e) =>
119
+ `<a href="#" class="vg-nav ${e.canon === this.canon ? 'sel' : ''}"
120
+ data-canon="${e.canon}">${e.canon}</a>`).join(' ')
121
+
122
+ this.innerHTML = `
123
+ <div class="vg-admin">
124
+ <nav>${nav}</nav>
125
+ <div id="vg-list"></div>
126
+ <div id="vg-form"></div>
127
+ </div>`
128
+
129
+ for (const a of this.querySelectorAll('.vg-nav')) {
130
+ a.onclick = (ev) => {
131
+ ev.preventDefault()
132
+ this.canon = a.dataset.canon
133
+ this.editing = null
134
+ this.render()
135
+ }
136
+ }
137
+
138
+ await this.renderList()
139
+ this.renderForm()
140
+ }
141
+
142
+ entdef() {
143
+ return this.entities.find((e) => e.canon === this.canon)
144
+ }
145
+
146
+ async renderList() {
147
+ const out = await bus.post('cmp:entity_admin,list:item',
148
+ { canon: this.canon })
149
+ const items = (out.ok && out.list) || []
150
+ const fields = Object.keys(this.entdef().fields)
151
+ .filter((f) => !SYS_FIELDS.includes(f))
152
+
153
+ const head = fields.map((f) => `<th>${f}</th>`).join('')
154
+ const rows = items.map((item) => `
155
+ <tr data-id="${item.id}">
156
+ ${fields.map((f) => `<td>${null == item[f] ? '' : item[f]}</td>`).join('')}
157
+ <td>
158
+ <button class="vg-edit" data-id="${item.id}">edit</button>
159
+ <button class="vg-del" data-id="${item.id}">delete</button>
160
+ </td>
161
+ </tr>`).join('')
162
+
163
+ this.querySelector('#vg-list').innerHTML = `
164
+ <h2>${this.canon} <button id="vg-new">new</button></h2>
165
+ <table class="vg-table">
166
+ <thead><tr>${head}<th></th></tr></thead>
167
+ <tbody>${rows}</tbody>
168
+ </table>
169
+ <p id="vg-count">${items.length} item${1 === items.length ? '' : 's'}</p>`
170
+
171
+ this.items = items
172
+ this.querySelector('#vg-new').onclick = () => {
173
+ this.editing = {}
174
+ this.renderForm()
175
+ }
176
+ for (const b of this.querySelectorAll('.vg-edit')) {
177
+ b.onclick = () => {
178
+ this.editing = this.items.find((i) => i.id === b.dataset.id)
179
+ this.renderForm()
180
+ }
181
+ }
182
+ for (const b of this.querySelectorAll('.vg-del')) {
183
+ b.onclick = async () => {
184
+ await bus.post('cmp:entity_admin,remove:item',
185
+ { canon: this.canon, id: b.dataset.id })
186
+ this.editing = null
187
+ await this.renderList()
188
+ this.renderForm()
189
+ }
190
+ }
191
+ }
192
+
193
+ renderForm() {
194
+ const box = this.querySelector('#vg-form')
195
+ if (null == this.editing) {
196
+ box.innerHTML = ''
197
+ return
198
+ }
199
+
200
+ const def = this.entdef()
201
+ const fields = Object.keys(def.fields)
202
+ .filter((f) => !SYS_FIELDS.includes(f))
203
+
204
+ box.innerHTML = `
205
+ <form class="vg-entity-form">
206
+ <h3>${this.editing.id ? 'Edit' : 'New'} ${this.canon}</h3>
207
+ ${fields.map((f) => `
208
+ <label>${(def.fields[f] && def.fields[f].label) || f}
209
+ ${inputFor(f, def.fields[f], this.editing[f])}
210
+ </label>`).join('')}
211
+ <button type="submit">Save</button>
212
+ <button type="button" id="vg-cancel">Cancel</button>
213
+ <div id="vg-form-err"></div>
214
+ </form>`
215
+
216
+ box.querySelector('#vg-cancel').onclick = () => {
217
+ this.editing = null
218
+ this.renderForm()
219
+ }
220
+
221
+ box.querySelector('form').onsubmit = async (ev) => {
222
+ ev.preventDefault()
223
+ const item = { ...(this.editing.id ? { id: this.editing.id } : {}) }
224
+ for (const f of fields) {
225
+ const input = box.querySelector(`[name="${f}"]`)
226
+ const kind = (def.fields[f] && def.fields[f].kind) || 'String'
227
+ if ('Boolean' === kind) {
228
+ item[f] = input.checked
229
+ }
230
+ else if ('' === input.value) {
231
+ // Blank optional field: leave unset (don't send an empty string;
232
+ // an optional/Skip field rejects '').
233
+ continue
234
+ }
235
+ else {
236
+ item[f] = 'Number' === kind ? Number(input.value) : input.value
237
+ }
238
+ }
239
+ const res = await bus.post('cmp:entity_admin,save:item',
240
+ { canon: this.canon, item })
241
+ if (!res.ok) {
242
+ box.querySelector('#vg-form-err').textContent =
243
+ 'save failed: ' + (res.why || '')
244
+ }
245
+ else {
246
+ this.editing = null
247
+ await this.renderList()
248
+ this.renderForm()
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ customElements.define('vg-entity-admin', VgEntityAdmin)
@@ -0,0 +1,30 @@
1
+ // App shell: shows the auth component always; the entity admin only when
2
+ // signed in. Reacts to bus auth events.
3
+
4
+ import { bus, onEvent } from '../bus.js'
5
+
6
+
7
+ class VgApp extends HTMLElement {
8
+ connectedCallback() {
9
+ this.innerHTML = `
10
+ <header><h1>$$Name$$</h1><vg-auth></vg-auth></header>
11
+ <main id="vg-main"></main>`
12
+
13
+ onEvent('auth', ({ user }) => this.setMain(user))
14
+
15
+ // Resolve the cookie session on load.
16
+ bus.post('cmp:auth,load:state')
17
+ }
18
+
19
+ setMain(user) {
20
+ const main = this.querySelector('#vg-main')
21
+ if (user) {
22
+ main.innerHTML = '<vg-entity-admin></vg-entity-admin>'
23
+ }
24
+ else {
25
+ main.innerHTML = '<p class="vg-hint">Sign in to manage your data.</p>'
26
+ }
27
+ }
28
+ }
29
+
30
+ customElements.define('vg-app', VgApp)
@@ -0,0 +1,97 @@
1
+ // Auth: a seneca plugin holding the signed-in principal, and the
2
+ // <vg-auth> web component (sign-in form / signed-in bar) driving it
3
+ // purely through bus messages.
4
+
5
+ import { bus, emit } from '../bus.js'
6
+
7
+
8
+ bus.use(function auth_cmp() {
9
+ const seneca = this
10
+
11
+ let user = null
12
+
13
+ seneca.add('cmp:auth,get:state', function (msg, reply) {
14
+ reply({ ok: true, user })
15
+ })
16
+
17
+ // Resolve current auth from the backend (cookie session).
18
+ seneca.add('cmp:auth,load:state', function (msg, reply) {
19
+ this.act('aim:req,on:auth,load:auth', function (err, out) {
20
+ user = (!err && out.ok && 'signedin' === out.state) ? out.user : null
21
+ emit('auth', { user })
22
+ reply({ ok: true, user })
23
+ })
24
+ })
25
+
26
+ seneca.add('cmp:auth,signin:user', function (msg, reply) {
27
+ this.act('aim:req,on:auth,signin:user', {
28
+ email: msg.email,
29
+ password: msg.password,
30
+ }, function (err, out) {
31
+ if (err || !out.ok) {
32
+ return reply({ ok: false, why: (out && out.why) || 'signin-failed' })
33
+ }
34
+ user = out.user
35
+ emit('auth', { user })
36
+ reply({ ok: true, user })
37
+ })
38
+ })
39
+
40
+ seneca.add('cmp:auth,signout:user', function (msg, reply) {
41
+ this.act('aim:req,on:auth,signout:user', function () {
42
+ user = null
43
+ emit('auth', { user })
44
+ reply({ ok: true })
45
+ })
46
+ })
47
+ })
48
+
49
+
50
+ class VgAuth extends HTMLElement {
51
+ connectedCallback() {
52
+ this.render()
53
+ }
54
+
55
+ async render() {
56
+ const state = await bus.post('cmp:auth,get:state')
57
+
58
+ if (state.user) {
59
+ this.innerHTML = `
60
+ <div class="vg-auth-bar">
61
+ <span>Signed in: <b>${state.user.email}</b></span>
62
+ <button id="vg-signout">Sign out</button>
63
+ </div>`
64
+ this.querySelector('#vg-signout').onclick = async () => {
65
+ await bus.post('cmp:auth,signout:user')
66
+ this.render()
67
+ }
68
+ }
69
+ else {
70
+ this.innerHTML = `
71
+ <form class="vg-auth-form">
72
+ <h2>Sign in</h2>
73
+ <label>Email <input name="email" type="email" required /></label>
74
+ <label>Password <input name="password" type="password" required /></label>
75
+ <button type="submit">Sign in</button>
76
+ <div class="vg-auth-err" id="vg-auth-err"></div>
77
+ </form>`
78
+ this.querySelector('form').onsubmit = async (ev) => {
79
+ ev.preventDefault()
80
+ const fd = new FormData(ev.target)
81
+ const res = await bus.post('cmp:auth,signin:user', {
82
+ email: fd.get('email'),
83
+ password: fd.get('password'),
84
+ })
85
+ if (!res.ok) {
86
+ this.querySelector('#vg-auth-err').textContent =
87
+ 'Sign in failed: ' + res.why
88
+ }
89
+ else {
90
+ this.render()
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ customElements.define('vg-auth', VgAuth)
@@ -0,0 +1,10 @@
1
+ // Todo SPA entry: web components on a Seneca service bus (experimental).
2
+ // No framework: each component is a custom element backed by a seneca
3
+ // plugin (state), and all data flows are bus messages - aim:* messages
4
+ // travel to the backend gateway via the seneca-browser fetch transport.
5
+
6
+ import './style.css'
7
+ import './bus.js'
8
+ import './cmp/auth.js'
9
+ import './cmp/admin.js'
10
+ import './cmp/app.js'
@@ -0,0 +1,26 @@
1
+ * { box-sizing: border-box; }
2
+ body { font: 15px/1.5 system-ui, sans-serif; margin: 0; color: #1c2733; }
3
+ header { display: flex; align-items: center; justify-content: space-between;
4
+ padding: 0.6rem 1.2rem; border-bottom: 2px solid #e3e8ee; }
5
+ header h1 { font-size: 1.2rem; margin: 0; }
6
+ main { padding: 1.2rem; max-width: 60rem; }
7
+
8
+ .vg-auth-bar { display: flex; gap: 0.8rem; align-items: center; }
9
+ .vg-auth-form { max-width: 22rem; display: grid; gap: 0.6rem; }
10
+ .vg-auth-form label { display: grid; gap: 0.2rem; }
11
+ .vg-auth-err { color: #b3261e; min-height: 1.2em; }
12
+
13
+ .vg-admin nav { margin-bottom: 1rem; }
14
+ .vg-admin nav a { margin-right: 0.8rem; text-decoration: none; }
15
+ .vg-admin nav a.sel { font-weight: 700; border-bottom: 2px solid #1c2733; }
16
+
17
+ .vg-table { border-collapse: collapse; width: 100%; }
18
+ .vg-table th, .vg-table td { text-align: left; padding: 0.35rem 0.6rem;
19
+ border-bottom: 1px solid #e3e8ee; }
20
+
21
+ .vg-entity-form { margin-top: 1.2rem; max-width: 26rem; display: grid;
22
+ gap: 0.6rem; padding: 1rem; border: 1px solid #e3e8ee; border-radius: 6px; }
23
+ .vg-entity-form label { display: grid; gap: 0.2rem; }
24
+
25
+ button { cursor: pointer; padding: 0.25rem 0.7rem; }
26
+ .vg-hint { color: #5b6b7b; }