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
16 changes: 0 additions & 16 deletions .npmignore

This file was deleted.

12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

### Unreleased

### [3.0.0-alpha.8.1] - 2026-03-15

- feat(zone): use DataTable for list, added search/limit options
- routes/zr: add extra data about ZR parse failures

### [3.0.0-alpha.8] - 2026-03-14

- lib/zone: add limit option
Expand Down Expand Up @@ -57,8 +62,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
[3.0.0-alpha.1]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.1
[3.0.0-alpha.2]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.2
[3.0.0-alpha.3]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.3
[3.0.0-alpha.4]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.4
[3.0.0-alpha.5]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.5
[3.0.0-alpha.6]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.6
[3.0.0-alpha.4]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.4
[3.0.0-alpha.5]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.5
[3.0.0-alpha.6]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.6
[3.0.0-alpha.7]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.7
[3.0.0-alpha.8]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.8
[3.0.0-alpha.8.1]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.8.1
2 changes: 1 addition & 1 deletion CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This handcrafted artisanal software is brought to you by:

| <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">15</a>)|
| <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">16</a>)|
| :---: |

<sub>this file is generated by [.release](https://github.com/msimerson/.release).
Expand Down
85 changes: 82 additions & 3 deletions lib/zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ import { mapToDbColumn } from './util.js'
const zoneDbMap = { id: 'nt_zone_id', gid: 'nt_group_id' }
const boolFields = ['deleted']

function applyZoneFilters(query, params, filters = {}) {
let nextQuery = query
const nextParams = [...params]

const append = (sql) => {
nextQuery += `${/\bWHERE\b/.test(nextQuery) ? ' AND' : ' WHERE'} ${sql}`
}

const search = typeof filters.search === 'string' ? filters.search.trim() : ''
if (search) {
append('(zone LIKE ? OR description LIKE ?)')
const wildcard = `%${search}%`
nextParams.push(wildcard, wildcard)
}

const zoneLike = typeof filters.zone_like === 'string' ? filters.zone_like.trim() : ''
if (zoneLike) {
append('zone LIKE ?')
nextParams.push(`%${zoneLike}%`)
}

const descriptionLike = typeof filters.description_like === 'string' ? filters.description_like.trim() : ''
if (descriptionLike) {
append('description LIKE ?')
nextParams.push(`%${descriptionLike}%`)
}

return [nextQuery, nextParams]
}

class Zone {
constructor() {
this.mysql = Mysql
Expand All @@ -20,12 +50,34 @@ class Zone {

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

const filters = {
search: args.search,
zone_like: args.zone_like,
description_like: args.description_like,
}
delete args.search
delete args.zone_like
delete args.description_like

const sortByMap = {
id: 'nt_zone_id',
zone: 'zone',
description: 'description',
last_modified: 'last_modified',
}
const sortBy = sortByMap[args.sort_by] ?? 'zone'
const sortDir = args.sort_dir === 'desc' ? 'DESC' : 'ASC'
delete args.sort_by
delete args.sort_dir

const limit = Number.isInteger(args.limit) ? args.limit : undefined
delete args.limit
const offset = Number.isInteger(args.offset) ? Math.max(0, args.offset) : 0
delete args.offset

const sqlLimit = limit === undefined ? '' : ` LIMIT ${Math.max(1, limit)}`
const sqlLimit = limit === undefined ? '' : ` LIMIT ${Math.max(1, limit)} OFFSET ${offset}`

const [query, params] = Mysql.select(
`SELECT nt_zone_id AS id
Expand All @@ -46,7 +98,10 @@ class Zone {
mapToDbColumn(args, zoneDbMap),
)

const rows = await Mysql.execute(`${query}${sqlLimit}`, params)
let [finalQuery, finalParams] = applyZoneFilters(query, params, filters)
finalQuery += ` ORDER BY ${sortBy} ${sortDir}`

const rows = await Mysql.execute(`${finalQuery}${sqlLimit}`, finalParams)
for (const row of rows) {
for (const b of boolFields) {
row[b] = row[b] === 1
Expand Down Expand Up @@ -77,6 +132,30 @@ class Zone {
return rows
}

async count(args = {}) {
args = JSON.parse(JSON.stringify(args))
args.deleted = args.deleted ?? false

const filters = {
search: args.search,
zone_like: args.zone_like,
description_like: args.description_like,
}
delete args.search
delete args.zone_like
delete args.description_like

const [query, params] = Mysql.select(
`SELECT COUNT(*) AS total
FROM nt_zone`,
mapToDbColumn(args, zoneDbMap),
)

const [finalQuery, finalParams] = applyZoneFilters(query, params, filters)
const rows = await Mysql.execute(finalQuery, finalParams)
return rows?.[0]?.total ?? 0
}

async put(args) {
if (!args.id) return false
const id = args.id
Expand Down
1 change: 0 additions & 1 deletion lib/zone_record.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ class ZoneRecord {
for (const b of boolFields) {
row[b] = row[b] === 1
}

if (args.deleted === false) delete row.deleted
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nictool/api",
"version": "3.0.0-alpha.8",
"version": "3.0.0-alpha.8.1",
"description": "NicTool API",
"main": "index.js",
"type": "module",
Expand Down
33 changes: 29 additions & 4 deletions routes/zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,45 @@ function ZoneRoutes(server) {
tags: ['api'],
},
handler: async (request, h) => {
const deleted = request.query.deleted === true
const getArgs = {
deleted: request.query.deleted === true ? 1 : 0,
limit: 1000,
deleted,
limit: Number.isInteger(request.query.limit) ? request.query.limit : 1000,
}
if (request.params.id) getArgs.id = parseInt(request.params.id, 10)
if (request.query.search) getArgs.search = request.query.search
if (Number.isInteger(request.query.offset)) getArgs.offset = request.query.offset
if (request.query.zone_like) getArgs.zone_like = request.query.zone_like
if (request.query.description_like) getArgs.description_like = request.query.description_like
if (request.query.sort_by) getArgs.sort_by = request.query.sort_by
if (request.query.sort_dir) getArgs.sort_dir = request.query.sort_dir

const zones = await Zone.get(getArgs)
const countArgs = {
deleted,
...(getArgs.id ? { id: getArgs.id } : {}),
...(getArgs.search ? { search: getArgs.search } : {}),
...(getArgs.zone_like ? { zone_like: getArgs.zone_like } : {}),
...(getArgs.description_like ? { description_like: getArgs.description_like } : {}),
}

const [zones, filtered, total] = await Promise.all([
Zone.get(getArgs),
Zone.count(countArgs),
Zone.count(getArgs.id ? { deleted, id: getArgs.id } : { deleted }),
])

return h
.response({
zone: zones,
meta: {
api: meta.api,
msg: `here's your zone(s)`,
pagination: {
total,
filtered,
limit: getArgs.limit,
offset: getArgs.offset ?? 0,
},
},
})
.code(200)
Expand Down Expand Up @@ -103,7 +128,7 @@ function ZoneRoutes(server) {
},
handler: async (request, h) => {
const zones = await Zone.get({
deleted: request.query.deleted === true ? 1 : 0,
deleted: request.query.deleted === true,
id: parseInt(request.params.id, 10),
})

Expand Down
10 changes: 10 additions & 0 deletions routes/zone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ describe('zone routes', () => {
assert.equal(res.result.zone[0].zone, nsCase.zone)
})

it('GET /zone?search=... returns DB matches', async () => {
const res = await server.inject({
method: 'GET',
url: '/zone?search=route.example',
headers: auth.headers,
})
assert.equal(res.statusCode, 200)
assert.ok(res.result.zone.some((z) => z.zone === nsCase.zone))
})

it(`POST /zone (${case2Id})`, async () => {
const testCase = JSON.parse(JSON.stringify(nsCase))
testCase.id = case2Id // make it unique
Expand Down
26 changes: 25 additions & 1 deletion routes/zone_record.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import validate from '@nictool/validate'

import ZoneRecord from '../lib/zone_record.js'
import Zone from '../lib/zone.js'
import { meta } from '../lib/util.js'

async function zoneRecordResponseFailAction(request, h, err) {
const detail = err?.details?.find(
(d) => Array.isArray(d.path) && d.path[0] === 'zone_record' && d.path[2] === 'owner',
)

if (detail) {
const index = detail.path[1]
const badRecord = request.response?.source?.zone_record?.[index]

if (badRecord) {
let zoneName = 'unknown'
if (Number.isInteger(badRecord.zid)) {
const zones = await Zone.get({ id: badRecord.zid, deleted: 0 })
if (zones.length > 0) zoneName = zones[0].zone
}

err.message = `${err.message}. Invalid zone record owner for zone "${zoneName}" (zone id: ${badRecord.zid ?? 'unknown'}, record id: ${badRecord.id ?? 'unknown'}, owner: "${badRecord.owner ?? 'unknown'}")`
}
}

throw err
}

function ZoneRecordRoutes(server) {
server.route([
{
Expand All @@ -15,7 +39,7 @@ function ZoneRecordRoutes(server) {
},
response: {
schema: validate.zone_record.GET_res,
failAction: 'log',
failAction: zoneRecordResponseFailAction,
},
tags: ['api'],
},
Expand Down
Loading