bajo-spatial 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,24 +1,47 @@
1
1
  /**
2
- * Plugin factory
2
+ * Plugin factory.
3
+ *
4
+ * **Never** call this function directly!!! It's only-meant to be called by the {@link https://ardhi.github.io/bajo|Bajo framework} during plugin initialization.
3
5
  *
4
6
  * @param {string} pkgName - NPM package name
5
- * @returns {class}
7
+ * @returns {class} BajoSpatial
6
8
  */
7
9
  async function factory (pkgName) {
8
10
  const me = this
9
11
 
10
12
  /**
11
- * BajoSpatial class
13
+ * BajoSpatial class definition
12
14
  *
13
15
  * @class
14
16
  */
15
17
  class BajoSpatial extends this.app.baseClass.Base {
18
+ /**
19
+ * Creates an instance of BajoSpatial.
20
+ */
16
21
  constructor () {
17
22
  super(pkgName, me.app)
23
+ /**
24
+ * @property {object} config - Configuration object
25
+ */
18
26
  this.config = {}
19
27
  }
20
28
 
21
- buildBboxQuery = async ({ bbox, query = {}, model, options = {} } = {}) => {
29
+ /**
30
+ * Builds a bounding box query for a given model.
31
+ *
32
+ * @async
33
+ * @method
34
+ * @param {object} params - Parameters for building the query.
35
+ * @param {string} params.bbox - Bounding box coordinates.
36
+ * @param {object} [params.query={}] - Existing query object to merge with.
37
+ * @param {object} params.model - {@link https://ardhi.github.io/dobo|Dobo model} to apply the query to.
38
+ * @param {object} [params.options={}] - Additional options for query building.
39
+ * @param {string} [params.options.bboxLatField='lat'] - Latitude field name in the model.
40
+ * @param {string} [params.options.bboxLngField='lng'] - Longitude field name in the model.
41
+ * @returns {object} - The modified query object with bounding box conditions.
42
+ */
43
+ buildBboxQuery = async (params = {}) => {
44
+ let { bbox, query = {}, model, options = {} } = params
22
45
  const { merge, isEmpty } = this.app.lib._
23
46
  const props = model.properties.map(item => item.name)
24
47
  const { bboxLatField = 'lat', bboxLngField = 'lng' } = options
@@ -42,6 +65,15 @@ async function factory (pkgName) {
42
65
  return query
43
66
  }
44
67
 
68
+ /**
69
+ * Gets the bounding box for a given country ID.
70
+ * Requires the `bajoCommonDb` and `dobo` plugins to be available. Otherwise, it will return undefined.
71
+ *
72
+ * @async
73
+ * @method
74
+ * @param {string} item - Country ID.
75
+ * @returns {Promise<string|undefined>} - The bounding box string or undefined if not found.
76
+ */
45
77
  getCountryBbox = async (item) => {
46
78
  item = item + ''
47
79
  if (item.includes(',')) return
@@ -52,6 +84,14 @@ async function factory (pkgName) {
52
84
  throw this.error('Invalid country bbox \'%s\'', item, { statusCode: 400 })
53
85
  }
54
86
 
87
+ /**
88
+ * Parses a bounding box input.
89
+ *
90
+ * @async
91
+ * @method
92
+ * @param {string} input - The bounding box input, either a country ID or coordinates.
93
+ * @returns {Promise<Array<number>|undefined>} - The parsed bounding box coordinates or undefined if not found.
94
+ */
55
95
  parseBbox = async (input) => {
56
96
  const { isSet } = this.app.lib.aneka
57
97
  if (input.length === 2 && !input.includes(',')) return await this.getCountryBbox(input)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bajo-spatial",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Spatial functions for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,7 +34,9 @@
34
34
  "geolib": "^3.3.4"
35
35
  },
36
36
  "devDependencies": {
37
+ "chai": "^6.2.2",
37
38
  "clean-jsdoc-theme": "^4.3.0",
38
- "jsdoc-plugin-intersection": "^1.0.4"
39
+ "jsdoc-plugin-intersection": "^1.0.4",
40
+ "mocha": "^11.7.6"
39
41
  }
40
42
  }
@@ -0,0 +1,181 @@
1
+ /* global describe, it, beforeEach */
2
+
3
+ import { expect } from 'chai'
4
+
5
+ import factory from '../index.js'
6
+
7
+ describe('BajoSpatial', () => {
8
+ let app
9
+ let BajoSpatial
10
+ let spatial
11
+
12
+ beforeEach(async () => {
13
+ app = {
14
+ lib: {
15
+ _: {
16
+ merge: (target, source) => Object.assign(target, source),
17
+ isEmpty: (value) => {
18
+ if (value == null) return true
19
+ if (Array.isArray(value) || typeof value === 'string') return value.length === 0
20
+ if (typeof value === 'object') return Object.keys(value).length === 0
21
+ return false
22
+ }
23
+ },
24
+ aneka: {
25
+ isSet: (value) => value !== null && value !== undefined
26
+ }
27
+ },
28
+ baseClass: {
29
+ Base: class Base {
30
+ constructor (pkgName, appRef) {
31
+ this.pkgName = pkgName
32
+ this.app = appRef
33
+ }
34
+
35
+ error (msg, item) {
36
+ const text = msg.replace('%s', item)
37
+ return new Error(text)
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ BajoSpatial = await factory.call({ app }, 'bajo-spatial')
44
+ spatial = new BajoSpatial()
45
+ })
46
+
47
+ it('initializes with empty config object', () => {
48
+ expect(spatial.config).to.deep.equal({})
49
+ })
50
+
51
+ it('parseBbox parses valid coordinates', async () => {
52
+ const result = await spatial.parseBbox('100,-10,120,10')
53
+
54
+ expect(result).to.deep.equal([100, -10, 120, 10])
55
+ })
56
+
57
+ it('parseBbox throws for invalid coordinates', async () => {
58
+ try {
59
+ await spatial.parseBbox('100,-10,200,10')
60
+ expect.fail('Expected parseBbox to throw')
61
+ } catch (err) {
62
+ expect(err.message).to.equal("Invalid bbox '100,-10,200,10'")
63
+ }
64
+ })
65
+
66
+ it('parseBbox resolves country code through getCountryBbox', async () => {
67
+ spatial.getCountryBbox = async (code) => {
68
+ expect(code).to.equal('ID')
69
+ return [95, -11, 141, 6]
70
+ }
71
+
72
+ const result = await spatial.parseBbox('ID')
73
+
74
+ expect(result).to.deep.equal([95, -11, 141, 6])
75
+ })
76
+
77
+ it('buildBboxQuery returns merged query when fields exist', async () => {
78
+ const model = { properties: [{ name: 'lat' }, { name: 'lng' }] }
79
+
80
+ const result = await spatial.buildBboxQuery({
81
+ bbox: '100,-10,120,10',
82
+ query: { status: 'active' },
83
+ model
84
+ })
85
+
86
+ expect(result).to.deep.equal({
87
+ status: 'active',
88
+ lng: { $gte: 100, $lte: 120 },
89
+ lat: { $gte: -10, $lte: 10 }
90
+ })
91
+ })
92
+
93
+ it('buildBboxQuery appends bbox query to $and', async () => {
94
+ const model = { properties: [{ name: 'lat' }, { name: 'lng' }] }
95
+ const query = { $and: [{ status: 'active' }] }
96
+
97
+ const result = await spatial.buildBboxQuery({
98
+ bbox: '100,-10,120,10',
99
+ query,
100
+ model
101
+ })
102
+
103
+ expect(result.$and).to.have.lengthOf(2)
104
+ expect(result.$and[1]).to.deep.equal({
105
+ lng: { $gte: 100, $lte: 120 },
106
+ lat: { $gte: -10, $lte: 10 }
107
+ })
108
+ })
109
+
110
+ it('buildBboxQuery converts $or query into $and with bbox query', async () => {
111
+ const model = { properties: [{ name: 'lat' }, { name: 'lng' }] }
112
+ const query = { $or: [{ status: 'active' }, { status: 'pending' }] }
113
+
114
+ const result = await spatial.buildBboxQuery({
115
+ bbox: '100,-10,120,10',
116
+ query,
117
+ model
118
+ })
119
+
120
+ expect(result).to.deep.equal({
121
+ $and: [
122
+ [{ status: 'active' }, { status: 'pending' }],
123
+ {
124
+ lng: { $gte: 100, $lte: 120 },
125
+ lat: { $gte: -10, $lte: 10 }
126
+ }
127
+ ]
128
+ })
129
+ })
130
+
131
+ it('buildBboxQuery returns original query when model fields are missing', async () => {
132
+ const model = { properties: [{ name: 'latitude' }, { name: 'longitude' }] }
133
+ const query = { status: 'active' }
134
+
135
+ const result = await spatial.buildBboxQuery({
136
+ bbox: '100,-10,120,10',
137
+ query,
138
+ model
139
+ })
140
+
141
+ expect(result).to.deep.equal({ status: 'active' })
142
+ })
143
+
144
+ it('getCountryBbox returns undefined for direct bbox string or missing dependencies', async () => {
145
+ expect(await spatial.getCountryBbox('1,2,3,4')).to.equal(undefined)
146
+ expect(await spatial.getCountryBbox('ID')).to.equal(undefined)
147
+ })
148
+
149
+ it('getCountryBbox returns bbox from CdbCountry model', async () => {
150
+ app.bajoCommonDb = {}
151
+ app.dobo = {
152
+ getModel: () => ({
153
+ getRecord: async (id, opts) => {
154
+ expect(id).to.equal('ID')
155
+ expect(opts).to.deep.equal({ throwNotFound: false })
156
+ return { bbox: '95,-11,141,6' }
157
+ }
158
+ })
159
+ }
160
+
161
+ const result = await spatial.getCountryBbox('ID')
162
+
163
+ expect(result).to.equal('95,-11,141,6')
164
+ })
165
+
166
+ it('getCountryBbox throws when country id is unknown', async () => {
167
+ app.bajoCommonDb = {}
168
+ app.dobo = {
169
+ getModel: () => ({
170
+ getRecord: async () => null
171
+ })
172
+ }
173
+
174
+ try {
175
+ await spatial.getCountryBbox('ID')
176
+ expect.fail('Expected getCountryBbox to throw')
177
+ } catch (err) {
178
+ expect(err.message).to.equal("Invalid country bbox 'ID'")
179
+ }
180
+ })
181
+ })
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-06-27
4
+
5
+ - [2.3.0] Add documentations
6
+ - [2.3.0] Add tests
7
+
3
8
  ## 2026-01-07
4
9
 
5
10
  - [2.2.0] Ported to match ```bajo@2.2.x``` specs