Skip to content

Commit 1122146

Browse files
authored
Release v3.0.0-alpha.8.1 (#36)
- feat(zone): use DataTable for list, added search/limit options - routes/zr: add extra data about ZR parse failures
1 parent 400c12a commit 1122146

File tree

9 files changed

+157
-30
lines changed

9 files changed

+157
-30
lines changed

.npmignore

Lines changed: 0 additions & 16 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
66

77
### Unreleased
88

9+
### [3.0.0-alpha.8.1] - 2026-03-15
10+
11+
- feat(zone): use DataTable for list, added search/limit options
12+
- routes/zr: add extra data about ZR parse failures
13+
914
### [3.0.0-alpha.8] - 2026-03-14
1015

1116
- lib/zone: add limit option
@@ -57,8 +62,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
5762
[3.0.0-alpha.1]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.1
5863
[3.0.0-alpha.2]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.2
5964
[3.0.0-alpha.3]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.3
60-
[3.0.0-alpha.4]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.4
61-
[3.0.0-alpha.5]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.5
62-
[3.0.0-alpha.6]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.6
65+
[3.0.0-alpha.4]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.4
66+
[3.0.0-alpha.5]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.5
67+
[3.0.0-alpha.6]: https://github.com/NicTool/api/releases/tag/3.0.0-alpha.6
6368
[3.0.0-alpha.7]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.7
6469
[3.0.0-alpha.8]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.8
70+
[3.0.0-alpha.8.1]: https://github.com/NicTool/api/releases/tag/v3.0.0-alpha.8.1

CONTRIBUTORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This handcrafted artisanal software is brought to you by:
44

5-
| <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>)|
5+
| <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>)|
66
| :---: |
77

88
<sub>this file is generated by [.release](https://github.com/msimerson/.release).

lib/zone.js

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ import { mapToDbColumn } from './util.js'
44
const zoneDbMap = { id: 'nt_zone_id', gid: 'nt_group_id' }
55
const boolFields = ['deleted']
66

7+
function applyZoneFilters(query, params, filters = {}) {
8+
let nextQuery = query
9+
const nextParams = [...params]
10+
11+
const append = (sql) => {
12+
nextQuery += `${/\bWHERE\b/.test(nextQuery) ? ' AND' : ' WHERE'} ${sql}`
13+
}
14+
15+
const search = typeof filters.search === 'string' ? filters.search.trim() : ''
16+
if (search) {
17+
append('(zone LIKE ? OR description LIKE ?)')
18+
const wildcard = `%${search}%`
19+
nextParams.push(wildcard, wildcard)
20+
}
21+
22+
const zoneLike = typeof filters.zone_like === 'string' ? filters.zone_like.trim() : ''
23+
if (zoneLike) {
24+
append('zone LIKE ?')
25+
nextParams.push(`%${zoneLike}%`)
26+
}
27+
28+
const descriptionLike = typeof filters.description_like === 'string' ? filters.description_like.trim() : ''
29+
if (descriptionLike) {
30+
append('description LIKE ?')
31+
nextParams.push(`%${descriptionLike}%`)
32+
}
33+
34+
return [nextQuery, nextParams]
35+
}
36+
737
class Zone {
838
constructor() {
939
this.mysql = Mysql
@@ -20,12 +50,34 @@ class Zone {
2050

2151
async get(args) {
2252
args = JSON.parse(JSON.stringify(args))
23-
if (args.deleted === undefined) args.deleted = false
53+
args.deleted = args.deleted ?? false
54+
55+
const filters = {
56+
search: args.search,
57+
zone_like: args.zone_like,
58+
description_like: args.description_like,
59+
}
60+
delete args.search
61+
delete args.zone_like
62+
delete args.description_like
63+
64+
const sortByMap = {
65+
id: 'nt_zone_id',
66+
zone: 'zone',
67+
description: 'description',
68+
last_modified: 'last_modified',
69+
}
70+
const sortBy = sortByMap[args.sort_by] ?? 'zone'
71+
const sortDir = args.sort_dir === 'desc' ? 'DESC' : 'ASC'
72+
delete args.sort_by
73+
delete args.sort_dir
2474

2575
const limit = Number.isInteger(args.limit) ? args.limit : undefined
2676
delete args.limit
77+
const offset = Number.isInteger(args.offset) ? Math.max(0, args.offset) : 0
78+
delete args.offset
2779

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

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

49-
const rows = await Mysql.execute(`${query}${sqlLimit}`, params)
101+
let [finalQuery, finalParams] = applyZoneFilters(query, params, filters)
102+
finalQuery += ` ORDER BY ${sortBy} ${sortDir}`
103+
104+
const rows = await Mysql.execute(`${finalQuery}${sqlLimit}`, finalParams)
50105
for (const row of rows) {
51106
for (const b of boolFields) {
52107
row[b] = row[b] === 1
@@ -77,6 +132,30 @@ class Zone {
77132
return rows
78133
}
79134

135+
async count(args = {}) {
136+
args = JSON.parse(JSON.stringify(args))
137+
args.deleted = args.deleted ?? false
138+
139+
const filters = {
140+
search: args.search,
141+
zone_like: args.zone_like,
142+
description_like: args.description_like,
143+
}
144+
delete args.search
145+
delete args.zone_like
146+
delete args.description_like
147+
148+
const [query, params] = Mysql.select(
149+
`SELECT COUNT(*) AS total
150+
FROM nt_zone`,
151+
mapToDbColumn(args, zoneDbMap),
152+
)
153+
154+
const [finalQuery, finalParams] = applyZoneFilters(query, params, filters)
155+
const rows = await Mysql.execute(finalQuery, finalParams)
156+
return rows?.[0]?.total ?? 0
157+
}
158+
80159
async put(args) {
81160
if (!args.id) return false
82161
const id = args.id

lib/zone_record.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ class ZoneRecord {
5858
for (const b of boolFields) {
5959
row[b] = row[b] === 1
6060
}
61-
6261
if (args.deleted === false) delete row.deleted
6362
}
6463

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nictool/api",
3-
"version": "3.0.0-alpha.8",
3+
"version": "3.0.0-alpha.8.1",
44
"description": "NicTool API",
55
"main": "index.js",
66
"type": "module",

routes/zone.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,45 @@ function ZoneRoutes(server) {
3838
tags: ['api'],
3939
},
4040
handler: async (request, h) => {
41+
const deleted = request.query.deleted === true
4142
const getArgs = {
42-
deleted: request.query.deleted === true ? 1 : 0,
43-
limit: 1000,
43+
deleted,
44+
limit: Number.isInteger(request.query.limit) ? request.query.limit : 1000,
4445
}
4546
if (request.params.id) getArgs.id = parseInt(request.params.id, 10)
47+
if (request.query.search) getArgs.search = request.query.search
48+
if (Number.isInteger(request.query.offset)) getArgs.offset = request.query.offset
49+
if (request.query.zone_like) getArgs.zone_like = request.query.zone_like
50+
if (request.query.description_like) getArgs.description_like = request.query.description_like
51+
if (request.query.sort_by) getArgs.sort_by = request.query.sort_by
52+
if (request.query.sort_dir) getArgs.sort_dir = request.query.sort_dir
4653

47-
const zones = await Zone.get(getArgs)
54+
const countArgs = {
55+
deleted,
56+
...(getArgs.id ? { id: getArgs.id } : {}),
57+
...(getArgs.search ? { search: getArgs.search } : {}),
58+
...(getArgs.zone_like ? { zone_like: getArgs.zone_like } : {}),
59+
...(getArgs.description_like ? { description_like: getArgs.description_like } : {}),
60+
}
61+
62+
const [zones, filtered, total] = await Promise.all([
63+
Zone.get(getArgs),
64+
Zone.count(countArgs),
65+
Zone.count(getArgs.id ? { deleted, id: getArgs.id } : { deleted }),
66+
])
4867

4968
return h
5069
.response({
5170
zone: zones,
5271
meta: {
5372
api: meta.api,
5473
msg: `here's your zone(s)`,
74+
pagination: {
75+
total,
76+
filtered,
77+
limit: getArgs.limit,
78+
offset: getArgs.offset ?? 0,
79+
},
5580
},
5681
})
5782
.code(200)
@@ -103,7 +128,7 @@ function ZoneRoutes(server) {
103128
},
104129
handler: async (request, h) => {
105130
const zones = await Zone.get({
106-
deleted: request.query.deleted === true ? 1 : 0,
131+
deleted: request.query.deleted === true,
107132
id: parseInt(request.params.id, 10),
108133
})
109134

routes/zone.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ describe('zone routes', () => {
5454
assert.equal(res.result.zone[0].zone, nsCase.zone)
5555
})
5656

57+
it('GET /zone?search=... returns DB matches', async () => {
58+
const res = await server.inject({
59+
method: 'GET',
60+
url: '/zone?search=route.example',
61+
headers: auth.headers,
62+
})
63+
assert.equal(res.statusCode, 200)
64+
assert.ok(res.result.zone.some((z) => z.zone === nsCase.zone))
65+
})
66+
5767
it(`POST /zone (${case2Id})`, async () => {
5868
const testCase = JSON.parse(JSON.stringify(nsCase))
5969
testCase.id = case2Id // make it unique

routes/zone_record.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
import validate from '@nictool/validate'
22

33
import ZoneRecord from '../lib/zone_record.js'
4+
import Zone from '../lib/zone.js'
45
import { meta } from '../lib/util.js'
56

7+
async function zoneRecordResponseFailAction(request, h, err) {
8+
const detail = err?.details?.find(
9+
(d) => Array.isArray(d.path) && d.path[0] === 'zone_record' && d.path[2] === 'owner',
10+
)
11+
12+
if (detail) {
13+
const index = detail.path[1]
14+
const badRecord = request.response?.source?.zone_record?.[index]
15+
16+
if (badRecord) {
17+
let zoneName = 'unknown'
18+
if (Number.isInteger(badRecord.zid)) {
19+
const zones = await Zone.get({ id: badRecord.zid, deleted: 0 })
20+
if (zones.length > 0) zoneName = zones[0].zone
21+
}
22+
23+
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'}")`
24+
}
25+
}
26+
27+
throw err
28+
}
29+
630
function ZoneRecordRoutes(server) {
731
server.route([
832
{
@@ -15,7 +39,7 @@ function ZoneRecordRoutes(server) {
1539
},
1640
response: {
1741
schema: validate.zone_record.GET_res,
18-
failAction: 'log',
42+
failAction: zoneRecordResponseFailAction,
1943
},
2044
tags: ['api'],
2145
},

0 commit comments

Comments
 (0)