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'], },