diff --git a/.release b/.release
index d106d83..648aa35 160000
--- a/.release
+++ b/.release
@@ -1 +1 @@
-Subproject commit d106d83e69d5d8c99e4d6be4ba98ddde550d082b
+Subproject commit 648aa356cba200c1a46f4bda9e29871d1e9ba557
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c519682..c1f65fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
### Unreleased
+### [3.0.0-alpha.8] - 2026-03-14
+
+- lib/zone: add limit option
+- lib/nameserver.js: handle null fields from DB
+- routes/zone: report zone name on validation failure
+
### [3.0.0-alpha.7] - 2026-03-13
- fixes
@@ -36,7 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- feat(lib/nameserver): added, with tests
- feat(routes/nameserver): added, with tests
-### 3.0.0-alpha.3
+### [3.0.0-alpha.3]
- routes/permission: added GET, POST, DELETE
- permission.get: default search with deleted=0
@@ -55,3 +61,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
[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.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
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a489471..3547fbb 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -2,7 +2,7 @@
This handcrafted artisanal software is brought to you by:
-| 
msimerson (14)|
+| 
msimerson (15)|
| :---: |
this file is generated by [.release](https://github.com/msimerson/.release).
diff --git a/lib/nameserver.js b/lib/nameserver.js
index 84a99f3..cc9fc3c 100644
--- a/lib/nameserver.js
+++ b/lib/nameserver.js
@@ -103,11 +103,11 @@ function dbToObject(rows) {
}
for (const f of ['export']) {
for (const p of ['type', 'interval', 'serials', 'status']) {
- if (row[`${f}_${p}`] !== undefined) {
+ if (![null, undefined].includes(row[`${f}_${p}`])) {
if (row[f] === undefined) row[f] = {}
row[f][p] = row[`${f}_${p}`]
- delete row[`${f}_${p}`]
}
+ delete row[`${f}_${p}`]
}
}
}
diff --git a/lib/nameserver.test.js b/lib/nameserver.test.js
index 173a19c..b4d8113 100644
--- a/lib/nameserver.test.js
+++ b/lib/nameserver.test.js
@@ -33,6 +33,21 @@ describe('nameserver', function () {
assert.ok(await Nameserver.put({ id: testCase.id, name: testCase.name }))
})
+ it('handles null export interval gracefully', async () => {
+ await Nameserver.mysql.execute(
+ 'UPDATE nt_nameserver SET export_interval = NULL WHERE nt_nameserver_id = ?',
+ [testCase.id],
+ )
+
+ const ns = await Nameserver.get({ id: testCase.id })
+ assert.equal(ns[0].export.interval, undefined)
+
+ await Nameserver.mysql.execute(
+ 'UPDATE nt_nameserver SET export_interval = ? WHERE nt_nameserver_id = ?',
+ [0, testCase.id],
+ )
+ })
+
it('deletes a nameserver', async () => {
assert.ok(await Nameserver.delete({ id: testCase.id }))
let g = await Nameserver.get({ id: testCase.id, deleted: 1 })
diff --git a/lib/util.js b/lib/util.js
index 469b2eb..10945e8 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -7,8 +7,8 @@ function setEnv() {
/* c8 ignore next 9 */
switch (os.hostname()) {
- case 'mbp.simerson.net':
- case 'imac27.simerson.net':
+ case 'mattbook-m3.home.simerson.net':
+ case 'imac27.home.simerson.net':
process.env.NODE_ENV = 'development'
break
default:
diff --git a/lib/zone.js b/lib/zone.js
index 44e065e..e3250b0 100644
--- a/lib/zone.js
+++ b/lib/zone.js
@@ -22,9 +22,13 @@ class Zone {
args = JSON.parse(JSON.stringify(args))
if (args.deleted === undefined) args.deleted = false
- const rows = await Mysql.execute(
- ...Mysql.select(
- `SELECT nt_zone_id AS id
+ const limit = Number.isInteger(args.limit) ? args.limit : undefined
+ delete args.limit
+
+ const sqlLimit = limit === undefined ? '' : ` LIMIT ${Math.max(1, limit)}`
+
+ const [query, params] = Mysql.select(
+ `SELECT nt_zone_id AS id
, nt_group_id AS gid
, zone
, mailaddr
@@ -39,9 +43,10 @@ class Zone {
, last_publish
, deleted
FROM nt_zone`,
- mapToDbColumn(args, zoneDbMap),
- ),
+ mapToDbColumn(args, zoneDbMap),
)
+
+ const rows = await Mysql.execute(`${query}${sqlLimit}`, params)
for (const row of rows) {
for (const b of boolFields) {
row[b] = row[b] === 1
@@ -49,6 +54,21 @@ class Zone {
for (const f of ['description', 'location']) {
if ([null].includes(row[f])) row[f] = ''
}
+
+ // Coerce legacy DB NULLs to sane defaults so responses validate
+ const zoneDefaults = {
+ minimum: 3600,
+ ttl: 3600,
+ refresh: 86400,
+ retry: 7200,
+ expire: 1209600,
+ }
+ for (const [f, val] of Object.entries(zoneDefaults)) {
+ if ([null, undefined].includes(row[f])) row[f] = val
+ }
+
+ if ([null, undefined].includes(row.serial)) row.serial = 0
+
if (row['last_publish'] === undefined) delete row['last_publish']
if (/00:00:00/.test(row['last_publish'])) row['last_publish'] = null
if (args.deleted === false) delete row.deleted
diff --git a/lib/zone.test.js b/lib/zone.test.js
index c4be676..99079d6 100644
--- a/lib/zone.test.js
+++ b/lib/zone.test.js
@@ -35,6 +35,18 @@ describe('zone', function () {
assert.ok(await Zone.put({ id: testCase.id, mailaddr: testCase.mailaddr }))
})
+ it('handles null minimum gracefully', async () => {
+ await Zone.mysql.execute('UPDATE nt_zone SET minimum = NULL WHERE nt_zone_id = ?', [testCase.id])
+
+ const z = await Zone.get({ id: testCase.id })
+ assert.equal(z[0].minimum, 3600)
+
+ await Zone.mysql.execute('UPDATE nt_zone SET minimum = ? WHERE nt_zone_id = ?', [
+ testCase.minimum,
+ testCase.id,
+ ])
+ })
+
describe('deletes a zone', async () => {
it('can delete a zone', async () => {
assert.ok(await Zone.delete({ id: testCase.id }))
diff --git a/package.json b/package.json
index a8d72c0..c9ef7ba 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,18 @@
{
"name": "@nictool/api",
- "version": "3.0.0-alpha.7",
+ "version": "3.0.0-alpha.8",
"description": "NicTool API",
"main": "index.js",
"type": "module",
- "files": [ "CHANGELOG.md", "conf.d", "html", "lib", "routes", "sql", "server.js" ],
+ "files": [
+ "CHANGELOG.md",
+ "conf.d",
+ "html",
+ "lib",
+ "routes",
+ "sql",
+ "server.js"
+ ],
"scripts": {
"format": "npm run lint:fix && npm run prettier:fix",
"lint": "npx eslint *.js **/*.js",
diff --git a/routes/zone.js b/routes/zone.js
index d046c0b..ea233dc 100644
--- a/routes/zone.js
+++ b/routes/zone.js
@@ -3,6 +3,24 @@ import validate from '@nictool/validate'
import Zone from '../lib/zone.js'
import { meta } from '../lib/util.js'
+function zoneResponseFailAction(request, h, err) {
+ const detail = err?.details?.find(
+ (d) => Array.isArray(d.path) && d.path[0] === 'zone' && d.path[2] === 'zone',
+ )
+
+ if (detail) {
+ const index = detail.path[1]
+ const badZone = request.response?.source?.zone?.[index]?.zone
+ const badId = request.response?.source?.zone?.[index]?.id
+
+ if (badZone !== undefined) {
+ err.message = `${err.message}. Invalid zone value: "${badZone}" (id: ${badId ?? 'unknown'})`
+ }
+ }
+
+ throw err
+}
+
function ZoneRoutes(server) {
server.route([
{
@@ -15,13 +33,14 @@ function ZoneRoutes(server) {
},
response: {
schema: validate.zone.GET_res,
- failAction: 'log',
+ failAction: zoneResponseFailAction,
},
tags: ['api'],
},
handler: async (request, h) => {
const getArgs = {
deleted: request.query.deleted === true ? 1 : 0,
+ limit: 1000,
}
if (request.params.id) getArgs.id = parseInt(request.params.id, 10)
@@ -48,7 +67,7 @@ function ZoneRoutes(server) {
},
response: {
schema: validate.zone.GET_res,
- failAction: 'log',
+ failAction: zoneResponseFailAction,
},
tags: ['api'],
},
@@ -78,7 +97,7 @@ function ZoneRoutes(server) {
},
response: {
schema: validate.zone.GET_res,
- failAction: 'log',
+ failAction: zoneResponseFailAction,
},
tags: ['api'],
},