Skip to content

Commit 55f22b4

Browse files
authored
Factories (#39)
- decorate user & group with permissions - add some missing PUT routes - better error handling validation errors - constraining views by GID - zone record factory & subclasses - zone factory & subclasses - user factory, toml, mysql, mongodb, elastic classes
1 parent f2bb416 commit 55f22b4

49 files changed

Lines changed: 2058 additions & 897 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ jobs:
4141
VERSION=$(node -e 'console.log(require("./package.json").version)')
4242
if printf '%s' "$VERSION" | grep -q -- '-'; then
4343
# prerelease versions get the "next" tag
44-
npm publish --access=public --tag=next
44+
npm publish --access public --tag next
4545
else
46-
npm publish --access=public
46+
npm publish --access public
4747
fi
4848
4949
publish-gpr:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,5 @@ dist
130130
.pnp.*
131131

132132
package-lock.json
133+
.release/
133134
conf.d/*.pem

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
66

77
### Unreleased
88

9+
### [3.0.0-alpha.11] - 2026-04-07
10+
11+
- decorate user & group with permissions
12+
- add some missing PUT routes
13+
- better error handling validation errors
14+
- constraining views by GID
15+
- zone record factory & subclasses
16+
- zone factory & subclasses
17+
- user factory, toml, mysql, mongodb, elastic classes
18+
919
### [3.0.0-alpha.10] - 2026-03-25
1020

1121
- config: replace .yaml with .toml
@@ -71,3 +81,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
7181
[3.0.0-alpha.8]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.8
7282
[3.0.0-alpha.9]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.9
7383
[3.0.0-alpha.10]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.10
84+
[3.0.0-alpha.11]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.11

CONTRIBUTORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This handcrafted artisanal software is brought to you by:
44

5-
| <img height="80" src="https://avatars.githubusercontent.com/u/261635?v=4"><br><a href="https://github.com/msimerson">msimerson</a> (<a href="https://github.com/NicTool/api/commits?author=msimerson">18</a>)|
5+
| <img height="80" src="https://avatars.githubusercontent.com/u/261635?v=4"><br><a href="https://github.com/msimerson">msimerson</a> (<a href="https://github.com/NicTool/api/commits?author=msimerson">19</a>)|
66
| :---: |
77

88
<sub>this file is generated by [.release](https://github.com/msimerson/.release).

lib/group.js

Lines changed: 0 additions & 69 deletions
This file was deleted.

lib/group/index.js

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import Mysql from '../mysql.js'
2+
import Permission from '../permission.js'
3+
import { mapToDbColumn } from '../util.js'
4+
5+
const groupDbMap = { id: 'nt_group_id', parent_gid: 'parent_group_id' }
6+
const boolFields = ['deleted']
7+
8+
class Group {
9+
constructor() {
10+
this.mysql = Mysql
11+
}
12+
13+
async create(args) {
14+
if (args.id) {
15+
const g = await this.get({ id: args.id })
16+
if (g.length === 1) return g[0].id
17+
}
18+
19+
const usable_ns = args.usable_ns
20+
delete args.usable_ns
21+
22+
const parent_gid = args.parent_gid ?? 0
23+
24+
const gid = await Mysql.execute(...Mysql.insert(`nt_group`, mapToDbColumn(args, groupDbMap)))
25+
26+
if (gid && parent_gid !== 0) {
27+
await this.addToSubgroups(gid, parent_gid)
28+
}
29+
30+
await Permission.create({
31+
gid,
32+
name: `Group ${args.name} perms`,
33+
nameserver: { usable: Array.isArray(usable_ns) ? usable_ns : [] },
34+
})
35+
36+
return gid
37+
}
38+
39+
async addToSubgroups(gid, parent_gid, rank = 1000) {
40+
if (!parent_gid || parent_gid === 0) return
41+
42+
await Mysql.execute(...Mysql.insert('nt_group_subgroups', {
43+
nt_group_id: parent_gid,
44+
nt_subgroup_id: gid,
45+
rank,
46+
}))
47+
48+
const parent = await this.get({ id: parent_gid })
49+
if (parent.length === 1 && parent[0].parent_gid !== 0) {
50+
await this.addToSubgroups(gid, parent[0].parent_gid, rank - 1)
51+
}
52+
}
53+
54+
async get(args_orig) {
55+
const args = JSON.parse(JSON.stringify(args_orig))
56+
if (args.deleted === undefined) args.deleted = false
57+
58+
const include_subgroups = args.include_subgroups === true
59+
delete args.include_subgroups
60+
61+
let query = `SELECT g.nt_group_id AS id
62+
, g.parent_group_id AS parent_gid
63+
, g.name
64+
, g.deleted
65+
FROM nt_group g`
66+
67+
const params = []
68+
const where = []
69+
70+
if (args.id) {
71+
if (include_subgroups) {
72+
const subgroupRows = await Mysql.execute(
73+
'SELECT nt_subgroup_id FROM nt_group_subgroups WHERE nt_group_id = ?',
74+
[args.id]
75+
)
76+
const gids = [args.id, ...subgroupRows.map(r => r.nt_subgroup_id)]
77+
where.push(`g.nt_group_id IN (${gids.join(',')})`)
78+
} else {
79+
where.push('g.nt_group_id = ?')
80+
params.push(args.id)
81+
}
82+
delete args.id
83+
}
84+
85+
if (args.parent_gid !== undefined) {
86+
where.push('g.parent_group_id = ?')
87+
params.push(args.parent_gid)
88+
delete args.parent_gid
89+
}
90+
91+
if (args.name) {
92+
where.push('g.name = ?')
93+
params.push(args.name)
94+
delete args.name
95+
}
96+
97+
if (args.deleted !== undefined) {
98+
where.push('g.deleted = ?')
99+
params.push(args.deleted ? 1 : 0)
100+
delete args.deleted
101+
}
102+
103+
if (where.length > 0) {
104+
query += ` WHERE ${where.join(' AND ')}`
105+
}
106+
107+
const groups = await Mysql.execute(query, params)
108+
109+
for (const row of groups) {
110+
for (const b of boolFields) {
111+
row[b] = row[b] === 1
112+
}
113+
if ([false, undefined].includes(args_orig.deleted)) delete row.deleted
114+
115+
const perm = await Permission.get({ gid: row.id })
116+
if (perm) {
117+
row.permissions = perm
118+
}
119+
}
120+
return groups
121+
}
122+
123+
async put(args) {
124+
if (!args.id) return false
125+
const id = args.id
126+
delete args.id
127+
128+
const usable_ns = args.usable_ns
129+
delete args.usable_ns
130+
131+
if (usable_ns !== undefined) {
132+
const perm = await Permission.get({ gid: id })
133+
if (perm) {
134+
await Permission.put({
135+
id: perm.id,
136+
nameserver: { usable: Array.isArray(usable_ns) ? usable_ns : [] }
137+
})
138+
}
139+
}
140+
141+
if (Object.keys(args).length === 0) return true
142+
143+
const r = await Mysql.execute(
144+
...Mysql.update(`nt_group`, `nt_group_id=${id}`, mapToDbColumn(args, groupDbMap)),
145+
)
146+
return r.changedRows === 1
147+
}
148+
149+
async delete(args) {
150+
const r = await Mysql.execute(
151+
...Mysql.update(`nt_group`, `nt_group_id=${args.id}`, {
152+
deleted: args.deleted ?? 1,
153+
}),
154+
)
155+
return r.changedRows === 1
156+
}
157+
158+
async destroy(args) {
159+
// Clean up associated permission rows before removing the group
160+
await Mysql.execute(`DELETE FROM nt_perm WHERE nt_group_id = ? AND nt_user_id IS NULL`, [args.id])
161+
const r = await Mysql.execute(...Mysql.delete(`nt_group`, { nt_group_id: args.id }))
162+
return r.affectedRows === 1
163+
}
164+
}
165+
166+
export default new Group()
Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import assert from 'node:assert/strict'
22
import { describe, it, after, before } from 'node:test'
33

4-
import Group from './group.js'
4+
import Group from './index.js'
55

6-
import testCase from './test/group.json' with { type: 'json' }
6+
import testCase from '../test/group.json' with { type: 'json' }
77

88
after(async () => {
99
Group.mysql.disconnect()
@@ -16,31 +16,25 @@ describe('group', function () {
1616

1717
it('gets group by id', async () => {
1818
const g = await Group.get({ id: testCase.id })
19-
assert.deepEqual(g[0], {
20-
id: testCase.id,
21-
name: testCase.name,
22-
parent_gid: 0,
23-
})
19+
assert.equal(g[0].id, testCase.id)
20+
assert.equal(g[0].name, testCase.name)
21+
assert.equal(g[0].parent_gid, 0)
22+
assert.ok(g[0].permissions, 'group has permissions')
2423
})
2524

2625
it('gets group by name', async () => {
2726
const g = await Group.get({ name: testCase.name })
28-
assert.deepEqual(g[0], {
29-
id: testCase.id,
30-
name: testCase.name,
31-
parent_gid: 0,
32-
})
27+
assert.equal(g[0].id, testCase.id)
28+
assert.equal(g[0].name, testCase.name)
29+
assert.equal(g[0].parent_gid, 0)
30+
assert.ok(g[0].permissions, 'group has permissions')
3331
})
3432

3533
it('changes a group', async () => {
3634
assert.ok(await Group.put({ id: testCase.id, name: 'example.net' }))
37-
assert.deepEqual(await Group.get({ id: testCase.id }), [
38-
{
39-
id: testCase.id,
40-
name: 'example.net',
41-
parent_gid: 0,
42-
},
43-
])
35+
const g = await Group.get({ id: testCase.id })
36+
assert.equal(g[0].id, testCase.id)
37+
assert.equal(g[0].name, 'example.net')
4438
assert.ok(await Group.put({ id: testCase.id, name: testCase.name }))
4539
})
4640

0 commit comments

Comments
 (0)