Skip to content

Commit eec83cc

Browse files
authored
group factories (#44)
get group class ready for a multi-db store future.
1 parent e3f4f5b commit eec83cc

21 files changed

Lines changed: 287 additions & 211 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ on:
44
pull_request:
55
types: [opened, synchronize, reopened]
66

7-
env:
8-
CI: true
9-
NODE_ENV: test
10-
NODE_VER: 22
11-
127
jobs:
138
lint:
149
permissions:
@@ -23,8 +18,6 @@ jobs:
2318
- name: Start MySQL
2419
run: sudo /etc/init.d/mysql start
2520
- uses: actions/setup-node@v4
26-
with:
27-
node-version: ${{ env.NODE_VER }}
2821
- uses: actions/checkout@v6
2922
- run: npm install
3023
- name: Initialize MySQL
@@ -90,7 +83,6 @@ jobs:
9083
brew services start mysql@8.4
9184
- uses: actions/checkout@v6
9285
- uses: actions/setup-node@v6
93-
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
9486
with:
9587
node-version: ${{ matrix.node-version }}
9688
- run: sh sql/init-mysql.sh
@@ -130,7 +122,6 @@ jobs:
130122
choco install mysql
131123
- uses: actions/checkout@v6
132124
- uses: actions/setup-node@v6
133-
name: Node ${{ matrix.node-version }}
134125
with:
135126
node-version: ${{ matrix.node-version }}
136127
- run: sh sql/init-mysql.sh

.github/workflows/publish.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ on:
66

77
env:
88
CI: true
9-
node-version: 22
109

1110
jobs:
1211
build:
@@ -17,8 +16,6 @@ jobs:
1716
- run: sudo /etc/init.d/mysql start
1817
- uses: actions/checkout@v6
1918
- uses: actions/setup-node@v6
20-
with:
21-
node-version: ${{ env.node-version }}
2219
- run: sh sql/init-mysql.sh
2320
- run: npm install
2421
- run: npm test
@@ -31,16 +28,12 @@ jobs:
3128
runs-on: ubuntu-latest
3229
steps:
3330
- uses: actions/setup-node@v6
34-
name: Node ${{ env.node-version }}
3531
with:
36-
node-version: ${{ env.node-version }}
3732
registry-url: https://registry.npmjs.org/
38-
3933
- uses: actions/checkout@v6
4034
with:
4135
fetch-depth: 0
4236
# fetch-depth 0 needed by GitHub Release
43-
4437
- name: publish to NPM
4538
run: |
4639
VERSION=$(node -e 'console.log(require("./package.json").version)')
@@ -61,7 +54,6 @@ jobs:
6154
- uses: actions/checkout@v6
6255
- uses: actions/setup-node@v6
6356
with:
64-
node-version: ${{ env.node-version }}
6557
registry-url: https://npm.pkg.github.com/
6658
scope: '@nictool'
6759
- run: npm publish

lib/group/index.js

Lines changed: 16 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,18 @@
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-
}
1+
const storeType = process.env.NICTOOL_DATA_STORE ?? 'mysql'
2+
3+
let RepoClass
4+
switch (storeType) {
5+
case 'toml':
6+
RepoClass = (await import('./store/toml.js')).default
7+
break
8+
case 'mongodb':
9+
RepoClass = (await import('./store/mongodb.js')).default
10+
break
11+
case 'elasticsearch':
12+
RepoClass = (await import('./store/elasticsearch.js')).default
13+
break
14+
default:
15+
RepoClass = (await import('./store/mysql.js')).default
16416
}
16517

166-
export default new Group()
18+
export default new RepoClass()

lib/group/store/base.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Group domain class – pure attributes and business logic.
3+
*
4+
* Has zero knowledge of how groups are persisted. All group repository classes
5+
* must extend this class and implement the repo contract methods.
6+
*
7+
* Repo contract:
8+
* get(args) → object[]
9+
* create(args) → number (groupId)
10+
* put(args) → boolean
11+
* delete(args) → boolean
12+
* destroy(args) → boolean
13+
*/
14+
class GroupBase {
15+
constructor(args = {}) {
16+
this.debug = args?.debug ?? false
17+
}
18+
19+
// -------------------------------------------------------------------------
20+
// Repo contract – subclasses must implement these
21+
// -------------------------------------------------------------------------
22+
23+
async get(_args) {
24+
throw new Error('get() not implemented by this repo')
25+
}
26+
27+
async create(_args) {
28+
throw new Error('create() not implemented by this repo')
29+
}
30+
31+
async put(_args) {
32+
throw new Error('put() not implemented by this repo')
33+
}
34+
35+
async delete(_args) {
36+
throw new Error('delete() not implemented by this repo')
37+
}
38+
39+
async destroy(_args) {
40+
throw new Error('destroy() not implemented by this repo')
41+
}
42+
}
43+
44+
export default GroupBase

lib/group/store/mongodb.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import GroupBase from './base.js'
2+
3+
class GroupRepoMongoDB extends GroupBase {
4+
async authenticate(_authTry) {
5+
throw new Error('GroupRepoMongoDB is not yet implemented')
6+
}
7+
8+
async get(_args) {
9+
throw new Error('GroupRepoMongoDB is not yet implemented')
10+
}
11+
12+
async create(_args) {
13+
throw new Error('GroupRepoMongoDB is not yet implemented')
14+
}
15+
16+
async put(_args) {
17+
throw new Error('GroupRepoMongoDB is not yet implemented')
18+
}
19+
20+
async delete(_args) {
21+
throw new Error('GroupRepoMongoDB is not yet implemented')
22+
}
23+
24+
async destroy(_args) {
25+
throw new Error('GroupRepoMongoDB is not yet implemented')
26+
}
27+
}
28+
29+
export default GroupRepoMongoDB

0 commit comments

Comments
 (0)