Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ on:
pull_request:
types: [opened, synchronize, reopened]

env:
CI: true
NODE_ENV: test
NODE_VER: 22

jobs:
lint:
permissions:
Expand All @@ -23,8 +18,6 @@ jobs:
- name: Start MySQL
run: sudo /etc/init.d/mysql start
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VER }}
- uses: actions/checkout@v6
- run: npm install
- name: Initialize MySQL
Expand Down Expand Up @@ -90,7 +83,6 @@ jobs:
brew services start mysql@8.4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
with:
node-version: ${{ matrix.node-version }}
- run: sh sql/init-mysql.sh
Expand Down Expand Up @@ -130,7 +122,6 @@ jobs:
choco install mysql
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
name: Node ${{ matrix.node-version }}
with:
node-version: ${{ matrix.node-version }}
- run: sh sql/init-mysql.sh
Expand Down
8 changes: 0 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:

env:
CI: true
node-version: 22

jobs:
build:
Expand All @@ -17,8 +16,6 @@ jobs:
- run: sudo /etc/init.d/mysql start
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.node-version }}
- run: sh sql/init-mysql.sh
- run: npm install
- run: npm test
Expand All @@ -31,16 +28,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v6
name: Node ${{ env.node-version }}
with:
node-version: ${{ env.node-version }}
registry-url: https://registry.npmjs.org/

- uses: actions/checkout@v6
with:
fetch-depth: 0
# fetch-depth 0 needed by GitHub Release

- name: publish to NPM
run: |
VERSION=$(node -e 'console.log(require("./package.json").version)')
Expand All @@ -61,7 +54,6 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.node-version }}
registry-url: https://npm.pkg.github.com/
scope: '@nictool'
- run: npm publish
Expand Down
180 changes: 16 additions & 164 deletions lib/group/index.js
Original file line number Diff line number Diff line change
@@ -1,166 +1,18 @@
import Mysql from '../mysql.js'
import Permission from '../permission.js'
import { mapToDbColumn } from '../util.js'

const groupDbMap = { id: 'nt_group_id', parent_gid: 'parent_group_id' }
const boolFields = ['deleted']

class Group {
constructor() {
this.mysql = Mysql
}

async create(args) {
if (args.id) {
const g = await this.get({ id: args.id })
if (g.length === 1) return g[0].id
}

const usable_ns = args.usable_ns
delete args.usable_ns

const parent_gid = args.parent_gid ?? 0

const gid = await Mysql.execute(...Mysql.insert(`nt_group`, mapToDbColumn(args, groupDbMap)))

if (gid && parent_gid !== 0) {
await this.addToSubgroups(gid, parent_gid)
}

await Permission.create({
gid,
name: `Group ${args.name} perms`,
nameserver: { usable: Array.isArray(usable_ns) ? usable_ns : [] },
})

return gid
}

async addToSubgroups(gid, parent_gid, rank = 1000) {
if (!parent_gid || parent_gid === 0) return

await Mysql.execute(...Mysql.insert('nt_group_subgroups', {
nt_group_id: parent_gid,
nt_subgroup_id: gid,
rank,
}))

const parent = await this.get({ id: parent_gid })
if (parent.length === 1 && parent[0].parent_gid !== 0) {
await this.addToSubgroups(gid, parent[0].parent_gid, rank - 1)
}
}

async get(args_orig) {
const args = JSON.parse(JSON.stringify(args_orig))
if (args.deleted === undefined) args.deleted = false

const include_subgroups = args.include_subgroups === true
delete args.include_subgroups

let query = `SELECT g.nt_group_id AS id
, g.parent_group_id AS parent_gid
, g.name
, g.deleted
FROM nt_group g`

const params = []
const where = []

if (args.id) {
if (include_subgroups) {
const subgroupRows = await Mysql.execute(
'SELECT nt_subgroup_id FROM nt_group_subgroups WHERE nt_group_id = ?',
[args.id]
)
const gids = [args.id, ...subgroupRows.map(r => r.nt_subgroup_id)]
where.push(`g.nt_group_id IN (${gids.join(',')})`)
} else {
where.push('g.nt_group_id = ?')
params.push(args.id)
}
delete args.id
}

if (args.parent_gid !== undefined) {
where.push('g.parent_group_id = ?')
params.push(args.parent_gid)
delete args.parent_gid
}

if (args.name) {
where.push('g.name = ?')
params.push(args.name)
delete args.name
}

if (args.deleted !== undefined) {
where.push('g.deleted = ?')
params.push(args.deleted ? 1 : 0)
delete args.deleted
}

if (where.length > 0) {
query += ` WHERE ${where.join(' AND ')}`
}

const groups = await Mysql.execute(query, params)

for (const row of groups) {
for (const b of boolFields) {
row[b] = row[b] === 1
}
if ([false, undefined].includes(args_orig.deleted)) delete row.deleted

const perm = await Permission.get({ gid: row.id })
if (perm) {
row.permissions = perm
}
}
return groups
}

async put(args) {
if (!args.id) return false
const id = args.id
delete args.id

const usable_ns = args.usable_ns
delete args.usable_ns

if (usable_ns !== undefined) {
const perm = await Permission.get({ gid: id })
if (perm) {
await Permission.put({
id: perm.id,
nameserver: { usable: Array.isArray(usable_ns) ? usable_ns : [] }
})
}
}

if (Object.keys(args).length === 0) return true

const r = await Mysql.execute(
...Mysql.update(`nt_group`, `nt_group_id=${id}`, mapToDbColumn(args, groupDbMap)),
)
return r.changedRows === 1
}

async delete(args) {
const r = await Mysql.execute(
...Mysql.update(`nt_group`, `nt_group_id=${args.id}`, {
deleted: args.deleted ?? 1,
}),
)
return r.changedRows === 1
}

async destroy(args) {
// Clean up associated permission rows before removing the group
await Mysql.execute(`DELETE FROM nt_perm WHERE nt_group_id = ? AND nt_user_id IS NULL`, [args.id])
const r = await Mysql.execute(...Mysql.delete(`nt_group`, { nt_group_id: args.id }))
return r.affectedRows === 1
}
const storeType = process.env.NICTOOL_DATA_STORE ?? 'mysql'

let RepoClass
switch (storeType) {
case 'toml':
RepoClass = (await import('./store/toml.js')).default
break
case 'mongodb':
RepoClass = (await import('./store/mongodb.js')).default
break
case 'elasticsearch':
RepoClass = (await import('./store/elasticsearch.js')).default
break
default:
RepoClass = (await import('./store/mysql.js')).default
}

export default new Group()
export default new RepoClass()
44 changes: 44 additions & 0 deletions lib/group/store/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Group domain class – pure attributes and business logic.
*
* Has zero knowledge of how groups are persisted. All group repository classes
* must extend this class and implement the repo contract methods.
*
* Repo contract:
* get(args) → object[]
* create(args) → number (groupId)
* put(args) → boolean
* delete(args) → boolean
* destroy(args) → boolean
*/
class GroupBase {
constructor(args = {}) {
this.debug = args?.debug ?? false
}

// -------------------------------------------------------------------------
// Repo contract – subclasses must implement these
// -------------------------------------------------------------------------

async get(_args) {
throw new Error('get() not implemented by this repo')
}

async create(_args) {
throw new Error('create() not implemented by this repo')
}

async put(_args) {
throw new Error('put() not implemented by this repo')
}

async delete(_args) {
throw new Error('delete() not implemented by this repo')
}

async destroy(_args) {
throw new Error('destroy() not implemented by this repo')
}
}

export default GroupBase
29 changes: 29 additions & 0 deletions lib/group/store/mongodb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import GroupBase from './base.js'

class GroupRepoMongoDB extends GroupBase {
async authenticate(_authTry) {
throw new Error('GroupRepoMongoDB is not yet implemented')
}

async get(_args) {
throw new Error('GroupRepoMongoDB is not yet implemented')
}

async create(_args) {
throw new Error('GroupRepoMongoDB is not yet implemented')
}

async put(_args) {
throw new Error('GroupRepoMongoDB is not yet implemented')
}

async delete(_args) {
throw new Error('GroupRepoMongoDB is not yet implemented')
}

async destroy(_args) {
throw new Error('GroupRepoMongoDB is not yet implemented')
}
}

export default GroupRepoMongoDB
Loading
Loading