@voxgig/build 4.4.0 → 4.11.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/README.md +51 -2
- package/build.ts +20 -0
- package/dist/api/api_gen.d.ts +7 -0
- package/dist/api/api_gen.js +370 -0
- package/dist/api/api_gen.js.map +1 -0
- package/dist/build.d.ts +16 -1
- package/dist/build.js +17 -1
- package/dist/build.js.map +1 -1
- package/dist/doc/doc_gen.d.ts +8 -0
- package/dist/doc/doc_gen.js +397 -0
- package/dist/doc/doc_gen.js.map +1 -0
- package/dist/env/web/web_gen.js +159 -1
- package/dist/env/web/web_gen.js.map +1 -1
- package/dist/shape/ent.js +4 -2
- package/dist/shape/ent.js.map +1 -1
- package/env/web/web_gen.ts +174 -1
- package/package.json +6 -9
- package/tm/web/backend/env/shared/seed.ts.frag +23 -0
- package/tm/web/backend/env/web/api.ts.frag +157 -0
- package/tm/web/backend/env/web/web.ts.frag +37 -4
- package/tm/web/backend/srv/api/api-srv.ts.frag +4 -0
- package/tm/web/backend/srv/api/expose.ts.frag +33 -0
- package/tm/web/backend/srv/api/get_info.ts.frag +6 -0
- package/tm/web/backend/srv/api/on_ent.ts.frag +124 -0
- package/tm/web/backend/srv/auth/apikey_util.ts.frag +10 -0
- package/tm/web/backend/srv/auth/auth-srv.ts.frag +1 -0
- package/tm/web/backend/srv/auth/change_pass.ts.frag +12 -0
- package/tm/web/backend/srv/auth/create_apikey.ts.frag +33 -0
- package/tm/web/backend/srv/auth/get_info.ts.frag +1 -0
- package/tm/web/backend/srv/auth/list_apikey.ts.frag +15 -0
- package/tm/web/backend/srv/auth/load_auth.ts.frag +10 -1
- package/tm/web/backend/srv/auth/remind_pass.ts.frag +35 -0
- package/tm/web/backend/srv/auth/revoke_apikey.ts.frag +23 -0
- package/tm/web/backend/srv/auth/signin_user.ts.frag +12 -2
- package/tm/web/backend/srv/auth/signout_user.ts.frag +11 -1
- package/tm/web/backend/srv/auth/update_user.ts.frag +15 -0
- package/tm/web/backend/srv/auth/user_util.ts.frag +21 -0
- package/tm/web/backend/srv/auth/web_change_pass.ts.frag +16 -0
- package/tm/web/backend/srv/auth/web_create_apikey.ts.frag +15 -0
- package/tm/web/backend/srv/auth/web_list_apikey.ts.frag +11 -0
- package/tm/web/backend/srv/auth/web_load_auth.ts.frag +1 -0
- package/tm/web/backend/srv/auth/web_remind_pass.ts.frag +7 -0
- package/tm/web/backend/srv/auth/web_revoke_apikey.ts.frag +14 -0
- package/tm/web/backend/srv/auth/web_signin_user.ts.frag +1 -0
- package/tm/web/backend/srv/auth/web_signout_user.ts.frag +11 -3
- package/tm/web/backend/srv/auth/web_update_user.ts.frag +11 -0
- package/tm/web/backend/srv/ent/access.ts.frag +179 -0
- package/tm/web/backend/srv/ent/cmd_list.ts.frag +86 -0
- package/tm/web/backend/srv/ent/cmd_load.ts.frag +56 -0
- package/tm/web/backend/srv/ent/cmd_remove.ts.frag +60 -0
- package/tm/web/backend/srv/ent/cmd_save.ts.frag +141 -0
- package/tm/web/backend/srv/ent/ent-srv.ts.frag +4 -0
- package/tm/web/backend/srv/ent/get_info.ts.frag +6 -0
- package/tm/web/backend/srv/ent/web_cmd_list.ts.frag +17 -0
- package/tm/web/backend/srv/ent/web_cmd_load.ts.frag +17 -0
- package/tm/web/backend/srv/ent/web_cmd_remove.ts.frag +17 -0
- package/tm/web/backend/srv/ent/web_cmd_save.ts.frag +17 -0
- package/tm/web/backend/test/unit/env/web/api-router.test.ts.frag +315 -0
- package/tm/web/backend/test/unit/env/web/surface.test.ts.frag +109 -0
- package/tm/web/backend/test/unit/srv/api/api.setup.ts.frag +162 -0
- package/tm/web/backend/test/unit/srv/api/api.test.ts.frag +225 -0
- package/tm/web/backend/test/unit/srv/auth/apikey.test.ts.frag +152 -0
- package/tm/web/backend/test/unit/srv/auth/auth.setup.ts.frag +40 -0
- package/tm/web/backend/test/unit/srv/auth/session.test.ts.frag +189 -0
- package/tm/web/backend/test/unit/srv/ent/ent.setup.ts.frag +53 -0
- package/tm/web/backend/test/unit/srv/ent/proxy.test.ts.frag +109 -0
- package/tm/web/docs/explanation/web-architecture.md.frag +50 -0
- package/tm/web/docs/how-to/add-a-custom-entity-view.md.frag +53 -0
- package/tm/web/docs/how-to/change-the-theme.md.frag +63 -0
- package/tm/web/docs/how-to/customise-the-web-app.md.frag +68 -0
- package/tm/web/docs/how-to/use-the-api.md.frag +53 -0
- package/tm/web/docs/reference/web-app.md.frag +98 -0
- package/tm/web/web/AGENTS.md.frag +52 -0
- package/tm/web/web/e2e/smoke.spec.js.frag +13 -11
- package/tm/web/web/index.html.frag +1 -0
- package/tm/web/web/package.json.frag +1 -0
- package/tm/web/web/playwright.config.js.frag +2 -1
- package/tm/web/web/src/api.js.frag +92 -0
- package/tm/web/web/src/bus.js.frag +14 -8
- package/tm/web/web/src/cmp/admin.js.frag +293 -212
- package/tm/web/web/src/cmp/admin.md.frag +51 -0
- package/tm/web/web/src/cmp/app.js.frag +12 -15
- package/tm/web/web/src/cmp/app.md.frag +28 -0
- package/tm/web/web/src/cmp/auth.js.frag +69 -41
- package/tm/web/web/src/cmp/auth.md.frag +36 -0
- package/tm/web/web/src/cmp/public.js.frag +58 -0
- package/tm/web/web/src/cmp/public.md.frag +26 -0
- package/tm/web/web/src/cmp/settings.js.frag +178 -0
- package/tm/web/web/src/cmp/settings.md.frag +30 -0
- package/tm/web/web/src/cmp/shell.js.frag +231 -0
- package/tm/web/web/src/cmp/shell.md.frag +49 -0
- package/tm/web/web/src/cmp/view/custom-view.js.frag +47 -0
- package/tm/web/web/src/cmp/view/custom-view.md.frag +31 -0
- package/tm/web/web/src/custom.css.frag +8 -0
- package/tm/web/web/src/customise.js.frag +30 -0
- package/tm/web/web/src/hooks.js.frag +79 -0
- package/tm/web/web/src/main.js.frag +17 -4
- package/tm/web/web/src/model.js.frag +162 -0
- package/tm/web/web/src/style.css.frag +135 -21
- package/tm/web/web/src/theme.js.frag +82 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
principalOf, validCanon, scopeOf, myProjectIds, ownerFor, asOwner, asSystem,
|
|
3
|
+
publicUser,
|
|
4
|
+
} from './access'
|
|
5
|
+
|
|
6
|
+
// aim:ent,cmd:list { ent:'zone/name', q?:{...} }
|
|
7
|
+
//
|
|
8
|
+
// Rows are filtered by @seneca/owner, not here: each list runs on a
|
|
9
|
+
// delegate whose custom.sysowner names the project, and owner refines the
|
|
10
|
+
// query by the tenant axis. The membership JOIN is the one thing owner
|
|
11
|
+
// cannot express (it scopes to ONE tenant, not "every project I belong
|
|
12
|
+
// to"), so listing across projects fans out over the member rows.
|
|
13
|
+
module.exports = function make_cmd_list() {
|
|
14
|
+
return async function cmd_list(this: any, msg: any, meta: any) {
|
|
15
|
+
const seneca = this
|
|
16
|
+
const model = seneca.context.model
|
|
17
|
+
const canon = String(msg.ent || '')
|
|
18
|
+
if (!validCanon(model, canon)) {
|
|
19
|
+
return { ok: false, why: 'unknown-entity' }
|
|
20
|
+
}
|
|
21
|
+
const user = principalOf(meta)
|
|
22
|
+
if (!user) {
|
|
23
|
+
return { ok: false, why: 'not-authenticated' }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const scope = scopeOf(model, canon)
|
|
27
|
+
const q = Object.assign({}, msg.q)
|
|
28
|
+
|
|
29
|
+
// sys/user: public projection, for reference pickers.
|
|
30
|
+
if ('open' === scope.kind) {
|
|
31
|
+
const list = await asSystem(seneca).entity(canon).list$(q)
|
|
32
|
+
return { ok: true, list: list.map(publicUser) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Not project data: owner scopes it to the acting user alone.
|
|
36
|
+
if ('user' === scope.kind) {
|
|
37
|
+
const owner = ownerFor(user, null)
|
|
38
|
+
return { ok: true, list: await asOwner(seneca, owner).entity(canon).list$(q) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Every id below came OUT of the member table, so membership is
|
|
42
|
+
// already established - ownerFor, not ownerOf, or each one costs a
|
|
43
|
+
// redundant round trip re-proving what this list just read.
|
|
44
|
+
const myIds = await myProjectIds(seneca, user.id)
|
|
45
|
+
|
|
46
|
+
// The projects themselves: one load per membership row.
|
|
47
|
+
if ('project' === scope.kind) {
|
|
48
|
+
const projects: any[] = []
|
|
49
|
+
for (const id of myIds) {
|
|
50
|
+
const p = await asOwner(seneca, ownerFor(user, id)).entity(canon).load$(id)
|
|
51
|
+
if (p && matches(p, q)) {
|
|
52
|
+
projects.push(p)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, list: projects }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 'scoped': one project asked for, or a union across all of mine.
|
|
59
|
+
const via = (scope as any).via
|
|
60
|
+
const ids = null != q[via] ? [q[via]] : myIds
|
|
61
|
+
if (null != q[via] && myIds.indexOf(q[via]) < 0) {
|
|
62
|
+
return { ok: false, why: 'forbidden' }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let all: any[] = []
|
|
66
|
+
for (const pid of ids) {
|
|
67
|
+
const sub = Object.assign({}, q)
|
|
68
|
+
sub[via] = pid
|
|
69
|
+
all = all.concat(
|
|
70
|
+
await asOwner(seneca, ownerFor(user, pid)).entity(canon).list$(sub))
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, list: all }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Simple exact-match filter (the membership-scoped project list is loaded
|
|
77
|
+
// by id, so the query can't go through list$).
|
|
78
|
+
function matches(item: any, q: any) {
|
|
79
|
+
for (const k of Object.keys(q || {})) {
|
|
80
|
+
if (item[k] !== q[k]) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
principalOf, validCanon, scopeOf, ownerOf, asOwner, asSystem, publicUser,
|
|
3
|
+
} from './access'
|
|
4
|
+
|
|
5
|
+
// aim:ent,cmd:load { ent:'zone/name', id }
|
|
6
|
+
//
|
|
7
|
+
// A load addresses a row by id, so which project it belongs to is not
|
|
8
|
+
// known from the message. Resolve that first (system read), then re-load
|
|
9
|
+
// as the caller so @seneca/owner is the thing that actually decides - a
|
|
10
|
+
// row outside the caller's project comes back null.
|
|
11
|
+
module.exports = function make_cmd_load() {
|
|
12
|
+
return async function cmd_load(this: any, msg: any, meta: any) {
|
|
13
|
+
const seneca = this
|
|
14
|
+
const model = seneca.context.model
|
|
15
|
+
const canon = String(msg.ent || '')
|
|
16
|
+
if (!validCanon(model, canon)) {
|
|
17
|
+
return { ok: false, why: 'unknown-entity' }
|
|
18
|
+
}
|
|
19
|
+
const user = principalOf(meta)
|
|
20
|
+
if (!user) {
|
|
21
|
+
return { ok: false, why: 'not-authenticated' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const scope = scopeOf(model, canon)
|
|
25
|
+
|
|
26
|
+
// sys/user: public projection, not project data.
|
|
27
|
+
if ('open' === scope.kind) {
|
|
28
|
+
const item = await asSystem(seneca).entity(canon).load$(msg.id)
|
|
29
|
+
return { ok: true, item: publicUser(item) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const found = await asSystem(seneca).entity(canon).load$(msg.id)
|
|
33
|
+
if (!found) {
|
|
34
|
+
return { ok: true, item: null }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const projectId = 'project' === scope.kind
|
|
38
|
+
? found.id
|
|
39
|
+
: found[(scope as any).via]
|
|
40
|
+
|
|
41
|
+
// A scoped row with no stored tenant is an anomaly, not user-scoped
|
|
42
|
+
// data - refuse it rather than let ownerOf downgrade to owner-only.
|
|
43
|
+
if ('scoped' === scope.kind && null == projectId) {
|
|
44
|
+
return { ok: false, why: 'forbidden' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const owner = await ownerOf(seneca, user, projectId)
|
|
48
|
+
if (null == owner) {
|
|
49
|
+
return { ok: false, why: 'forbidden' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const item = await asOwner(seneca, owner).entity(canon).load$(msg.id)
|
|
53
|
+
return null == item ? { ok: false, why: 'forbidden' } : { ok: true, item }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
principalOf, validCanon, scopeOf, ownerOf, asOwner, asSystem,
|
|
3
|
+
} from './access'
|
|
4
|
+
|
|
5
|
+
// aim:ent,cmd:remove { ent:'zone/name', id }
|
|
6
|
+
//
|
|
7
|
+
// Same shape as cmd_load: discover the row's project, then delete as the
|
|
8
|
+
// caller so @seneca/owner refines the query - a row outside the caller's
|
|
9
|
+
// project is simply not matched.
|
|
10
|
+
module.exports = function make_cmd_remove() {
|
|
11
|
+
return async function cmd_remove(this: any, msg: any, meta: any) {
|
|
12
|
+
const seneca = this
|
|
13
|
+
const model = seneca.context.model
|
|
14
|
+
const canon = String(msg.ent || '')
|
|
15
|
+
if (!validCanon(model, canon)) {
|
|
16
|
+
return { ok: false, why: 'unknown-entity' }
|
|
17
|
+
}
|
|
18
|
+
const user = principalOf(meta)
|
|
19
|
+
if (!user) {
|
|
20
|
+
return { ok: false, why: 'not-authenticated' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const scope = scopeOf(model, canon)
|
|
24
|
+
if ('open' === scope.kind) {
|
|
25
|
+
return { ok: false, why: 'read-only' }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const found = await asSystem(seneca).entity(canon).load$(msg.id)
|
|
29
|
+
if (!found) {
|
|
30
|
+
return { ok: true }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const projectId = 'project' === scope.kind
|
|
34
|
+
? found.id
|
|
35
|
+
: found[(scope as any).via]
|
|
36
|
+
|
|
37
|
+
// A scoped row with no stored tenant is an anomaly, not user-scoped
|
|
38
|
+
// data - refuse it rather than let ownerOf downgrade to owner-only.
|
|
39
|
+
if ('scoped' === scope.kind && null == projectId) {
|
|
40
|
+
return { ok: false, why: 'forbidden' }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const owner = await ownerOf(seneca, user, projectId)
|
|
44
|
+
if (null == owner) {
|
|
45
|
+
return { ok: false, why: 'forbidden' }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Owner treats a denied remove as a silent no-op, which would report
|
|
49
|
+
// success for a row the caller cannot see. Read it back as the caller
|
|
50
|
+
// first, so remove answers like load does.
|
|
51
|
+
const mine = asOwner(seneca, owner).entity(canon)
|
|
52
|
+
if (null == await mine.load$(msg.id)) {
|
|
53
|
+
return { ok: false, why: 'forbidden' }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await mine.remove$(msg.id)
|
|
57
|
+
return { ok: true }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
principalOf, validCanon, scopeOf, projectOf, ownerOf, asOwner, asSystem,
|
|
3
|
+
stripOwned, isDenied, OWNER_FIELDS,
|
|
4
|
+
} from './access'
|
|
5
|
+
|
|
6
|
+
// aim:ent,cmd:save { ent:'zone/name', item:{...} }
|
|
7
|
+
//
|
|
8
|
+
// Create or update. Row-level access is NOT enforced here: the save runs on
|
|
9
|
+
// a delegate carrying custom.sysowner, and @seneca/owner injects the
|
|
10
|
+
// ownership axes on create and refines the query on update - so an update
|
|
11
|
+
// naming a row outside the caller's project simply does not find it.
|
|
12
|
+
// Timestamps (t_c/t_m) are maintained by @seneca/entity-util.
|
|
13
|
+
//
|
|
14
|
+
// What this action must get right is WHICH TENANT the save acts in. On
|
|
15
|
+
// create the payload names it (there is no row to contradict). On update
|
|
16
|
+
// the tenant is read from the STORED row, and the payload's project field
|
|
17
|
+
// is pinned to it. Taking the tenant from the payload would let a caller
|
|
18
|
+
// name a project they DO belong to, pass the membership check, and have
|
|
19
|
+
// owner refine the query by that same project - overwriting someone else's
|
|
20
|
+
// row. This surface therefore does not move rows between projects.
|
|
21
|
+
module.exports = function make_cmd_save() {
|
|
22
|
+
return async function cmd_save(this: any, msg: any, meta: any) {
|
|
23
|
+
const seneca = this
|
|
24
|
+
const model = seneca.context.model
|
|
25
|
+
const canon = String(msg.ent || '')
|
|
26
|
+
if (!validCanon(model, canon)) {
|
|
27
|
+
return { ok: false, why: 'unknown-entity' }
|
|
28
|
+
}
|
|
29
|
+
const user = principalOf(meta)
|
|
30
|
+
if (!user) {
|
|
31
|
+
return { ok: false, why: 'not-authenticated' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scope = scopeOf(model, canon)
|
|
35
|
+
if ('open' === scope.kind) {
|
|
36
|
+
return { ok: false, why: 'read-only' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = Object.assign({}, msg.item)
|
|
40
|
+
|
|
41
|
+
// An id naming no existing row is a create with a client-chosen id, not
|
|
42
|
+
// an update: there is no stored tenant, and nothing to hijack.
|
|
43
|
+
const found = null == data.id
|
|
44
|
+
? null
|
|
45
|
+
: await asSystem(seneca).entity(canon).load$(data.id)
|
|
46
|
+
|
|
47
|
+
let projectId: string | null = null
|
|
48
|
+
|
|
49
|
+
if (null == found) {
|
|
50
|
+
projectId = projectOf(model, canon, msg)
|
|
51
|
+
if ('scoped' === scope.kind && null == projectId) {
|
|
52
|
+
return { ok: false, why: 'project-required' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A client-chosen id must move to `id$`. @seneca/owner takes the
|
|
56
|
+
// create path only when ent.id is null - with a plain id it treats
|
|
57
|
+
// the save as an update, finds no row, and fails save-not-found.
|
|
58
|
+
// `id$` is seneca-entity's "create with this id", so the row still
|
|
59
|
+
// goes through owner's create path and gets its axes injected.
|
|
60
|
+
if (null != data.id) {
|
|
61
|
+
data.id$ = data.id
|
|
62
|
+
delete data.id
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if ('project' === scope.kind) {
|
|
66
|
+
// The project IS the tenant, so updating one acts within itself.
|
|
67
|
+
projectId = found.id
|
|
68
|
+
}
|
|
69
|
+
else if ('scoped' === scope.kind) {
|
|
70
|
+
const via = (scope as any).via
|
|
71
|
+
projectId = found[via]
|
|
72
|
+
|
|
73
|
+
// A scoped row with no stored tenant is an anomaly (orphaned or
|
|
74
|
+
// legacy data), NOT user-scoped data. Refuse it rather than let
|
|
75
|
+
// ownerOf fall back to owner-only scoping, which would hand its
|
|
76
|
+
// creator access outside any project.
|
|
77
|
+
if (null == projectId) {
|
|
78
|
+
return { ok: false, why: 'forbidden' }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pin the tenant to the stored row. `via` is whatever field the
|
|
82
|
+
// model points at proj/project, so it is NOT necessarily one of the
|
|
83
|
+
// owner fields stripOwned removes - overwrite it explicitly, or a
|
|
84
|
+
// model naming it anything but project_id lets the payload move the
|
|
85
|
+
// row to another project.
|
|
86
|
+
data[via] = projectId
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const owner = await ownerOf(seneca, user, projectId)
|
|
90
|
+
if (null == owner) {
|
|
91
|
+
return { ok: false, why: 'forbidden' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The ownership axes come from the delegate, never the payload - a row
|
|
95
|
+
// loaded by the client carries them, and re-sending them unchanged
|
|
96
|
+
// would otherwise be read as a claim.
|
|
97
|
+
stripOwned(data)
|
|
98
|
+
|
|
99
|
+
// An update keeps the row's STORED ownership. stripOwned has just
|
|
100
|
+
// removed the client's copy, and owner injects any axis it finds
|
|
101
|
+
// null - so without this, one member editing another member's row
|
|
102
|
+
// silently transfers its owner_id to the editor (the `member` role
|
|
103
|
+
// relaxes writes on that axis, so the plugin raises no objection).
|
|
104
|
+
if (null != found) {
|
|
105
|
+
for (const f of OWNER_FIELDS) {
|
|
106
|
+
if (null != found[f]) {
|
|
107
|
+
data[f] = found[f]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let item: any
|
|
113
|
+
try {
|
|
114
|
+
item = await asOwner(seneca, owner).entity(canon).data$(data).save$()
|
|
115
|
+
}
|
|
116
|
+
catch (err: any) {
|
|
117
|
+
if (isDenied(err)) {
|
|
118
|
+
return { ok: false, why: 'forbidden' }
|
|
119
|
+
}
|
|
120
|
+
throw err
|
|
121
|
+
}
|
|
122
|
+
if (null == item) {
|
|
123
|
+
return { ok: false, why: 'forbidden' }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Creating a project makes the creator its first (owner) member. This
|
|
127
|
+
// is bookkeeping for a project the caller is not yet a member of, so
|
|
128
|
+
// it cannot run as `member` - see the `system` role in basic.ts.
|
|
129
|
+
if ('project' === scope.kind && null == found) {
|
|
130
|
+
await asSystem(seneca).entity('proj/member').data$({
|
|
131
|
+
project_id: item.id,
|
|
132
|
+
user_id: user.id,
|
|
133
|
+
role: 'owner',
|
|
134
|
+
owner_id: user.id,
|
|
135
|
+
}).save$()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ok: true, item }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Browser proxy: aim:web,on:ent,cmd:list -> aim:ent,cmd:list
|
|
2
|
+
//
|
|
3
|
+
// aim:web is the only namespace the gateway accepts from a browser, so
|
|
4
|
+
// every entity operation a browser may perform is declared as one of
|
|
5
|
+
// these proxies. The principal the gateway resolved travels with the
|
|
6
|
+
// message (meta.custom), so the real action scopes by membership.
|
|
7
|
+
module.exports = function make_web_cmd_list() {
|
|
8
|
+
return async function web_cmd_list(this: any, msg: any) {
|
|
9
|
+
return this.post('aim:ent,cmd:list', {
|
|
10
|
+
ent: msg.ent,
|
|
11
|
+
id: msg.id,
|
|
12
|
+
q: msg.q,
|
|
13
|
+
item: msg.item,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Browser proxy: aim:web,on:ent,cmd:load -> aim:ent,cmd:load
|
|
2
|
+
//
|
|
3
|
+
// aim:web is the only namespace the gateway accepts from a browser, so
|
|
4
|
+
// every entity operation a browser may perform is declared as one of
|
|
5
|
+
// these proxies. The principal the gateway resolved travels with the
|
|
6
|
+
// message (meta.custom), so the real action scopes by membership.
|
|
7
|
+
module.exports = function make_web_cmd_load() {
|
|
8
|
+
return async function web_cmd_load(this: any, msg: any) {
|
|
9
|
+
return this.post('aim:ent,cmd:load', {
|
|
10
|
+
ent: msg.ent,
|
|
11
|
+
id: msg.id,
|
|
12
|
+
q: msg.q,
|
|
13
|
+
item: msg.item,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Browser proxy: aim:web,on:ent,cmd:remove -> aim:ent,cmd:remove
|
|
2
|
+
//
|
|
3
|
+
// aim:web is the only namespace the gateway accepts from a browser, so
|
|
4
|
+
// every entity operation a browser may perform is declared as one of
|
|
5
|
+
// these proxies. The principal the gateway resolved travels with the
|
|
6
|
+
// message (meta.custom), so the real action scopes by membership.
|
|
7
|
+
module.exports = function make_web_cmd_remove() {
|
|
8
|
+
return async function web_cmd_remove(this: any, msg: any) {
|
|
9
|
+
return this.post('aim:ent,cmd:remove', {
|
|
10
|
+
ent: msg.ent,
|
|
11
|
+
id: msg.id,
|
|
12
|
+
q: msg.q,
|
|
13
|
+
item: msg.item,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Browser proxy: aim:web,on:ent,cmd:save -> aim:ent,cmd:save
|
|
2
|
+
//
|
|
3
|
+
// aim:web is the only namespace the gateway accepts from a browser, so
|
|
4
|
+
// every entity operation a browser may perform is declared as one of
|
|
5
|
+
// these proxies. The principal the gateway resolved travels with the
|
|
6
|
+
// message (meta.custom), so the real action scopes by membership.
|
|
7
|
+
module.exports = function make_web_cmd_save() {
|
|
8
|
+
return async function web_cmd_save(this: any, msg: any) {
|
|
9
|
+
return this.post('aim:ent,cmd:save', {
|
|
10
|
+
ent: msg.ent,
|
|
11
|
+
id: msg.id,
|
|
12
|
+
q: msg.q,
|
|
13
|
+
item: msg.item,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|