Skip to content

Commit d03b1dc

Browse files
committed
test(repo): add Deno tests and consolidate types
Adds a Deno test suite, moves shared types into src/Types.ts, and updates aliases/config so tests run locally and in CI.
1 parent d45fbb3 commit d03b1dc

8 files changed

Lines changed: 488 additions & 156 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ jobs:
2828

2929
- name: Type check
3030
run: deno check src/index.ts
31+
32+
- name: Test
33+
run: deno test --allow-read --allow-write --allow-env

build.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ export default defineBuildConfig({
1414
clean: true,
1515
/** Path alias for imports (e.g. @root → src). */
1616
alias: {
17-
'@root': resolve(__dirname, 'src'),
18-
'@interfaces': resolve(__dirname, 'src/interfaces')
17+
'@app': resolve(__dirname, 'src')
1918
},
2019
/** Rollup options: emit CJS and inline runtime deps. */
2120
rollup: {

deno.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,26 @@
4848
"useTabs": false
4949
},
5050
"lint": {
51-
"include": ["src/"],
51+
"include": ["src/", "tests/"],
5252
"rules": {
5353
"tags": ["fresh", "jsr", "jsx", "react", "recommended", "workspace"],
5454
"exclude": ["no-console", "no-external-import", "prefer-ascii", "prefer-primordials"]
5555
}
5656
},
5757
"lock": true,
5858
"nodeModulesDir": "auto",
59+
"test": {
60+
"include": ["tests/**/*.ts"],
61+
"exclude": ["tests/**/*.d.ts"]
62+
},
5963
"tasks": {
60-
"check": "deno fmt src/ && deno lint src/ && deno check src/"
64+
"check": "deno fmt src/ && deno lint src/ && deno check src/",
65+
"test": "deno fmt tests/ && deno lint tests/ && deno test --allow-read --allow-write --allow-env"
6166
},
6267
"imports": {
68+
"@std/assert": "jsr:@std/assert@^1.0.19",
6369
"@neabyte/jsonary": "./src/index.ts",
64-
"@root/": "./src/",
65-
"@interfaces/": "./src/interfaces/"
70+
"@app/": "./src/"
6671
},
6772
"publish": {
6873
"include": ["src/", "README.md", "LICENSE", "USAGE.md", "deno.json"]

src/Constant.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { QueryOperatorsType } from '@interfaces/index.ts'
1+
import type * as Types from '@app/Types.ts'
22

33
/**
4-
* Query operator constants.
4+
* Query operator constants
55
* @description Centralized definitions for all supported query operators.
66
*/
7-
export const queryOperators: QueryOperatorsType = {
7+
export const queryOperators: Types.QueryOperatorsType = {
88
eq: '=',
99
neq: '!=',
1010
gt: '>',
@@ -17,8 +17,8 @@ export const queryOperators: QueryOperatorsType = {
1717
} as const
1818

1919
/**
20-
* Gets all operator values sorted by length (longest first).
21-
* @description Used for parsing conditions with correct operator precedence.
20+
* Get sorted operator values
21+
* @description Sorts operators by length, longest first.
2222
* @returns Array of operator values sorted by length
2323
*/
2424
export function getOperatorsSorted(): string[] {

src/Query.ts

Lines changed: 89 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,87 @@
1-
import type { JsonaryCondition, JsonaryParent } from '@interfaces/index.ts'
2-
import { getOperatorsSorted, queryOperators } from '@root/Constant.ts'
1+
import type * as Types from '@app/Types.ts'
2+
import { getOperatorsSorted, queryOperators } from '@app/Constant.ts'
33

44
/**
5-
* Query builder for filtering and manipulating JSON data.
6-
* @description Provides fluent interface for building complex queries with chaining operations.
5+
* Query builder for records
6+
* @description Fluent chaining for filters, updates, deletes.
77
*/
88
export class QueryBuilder {
99
/** Cache for recently parsed conditions to improve performance */
10-
private static readonly conditionRecent: Map<string, JsonaryCondition | null> = new Map()
10+
private static readonly recentConditionCache: Map<string, Types.JsonaryCondition | null> =
11+
new Map()
1112
/** Parent instance for synchronization */
12-
private readonly parent: JsonaryParent | undefined
13+
private readonly parentDb: Types.JsonaryParent | undefined
1314
/** Array of applied conditions */
14-
private readonly conditions: JsonaryCondition[] = []
15+
private readonly conditions: Types.JsonaryCondition[] = []
1516
/** Original data array reference */
16-
private readonly originalData: Record<string, unknown>[]
17+
private readonly originalRecords: Record<string, unknown>[]
1718
/** Current filtered data array */
18-
private data: Record<string, unknown>[]
19+
private records: Record<string, unknown>[]
1920

2021
/**
21-
* Creates a new QueryBuilder instance.
22-
* @description Initializes the query builder with data and optional parent reference.
22+
* Create QueryBuilder instance
23+
* @description Stores records and optional parent sync.
2324
* @param data - Array of data records to query
2425
* @param parent - Optional parent instance for synchronization
2526
*/
26-
constructor(data: Record<string, unknown>[], parent?: JsonaryParent) {
27-
this.originalData = data
28-
this.data = [...data]
29-
this.parent = parent
27+
constructor(data: Record<string, unknown>[], parent?: Types.JsonaryParent) {
28+
this.originalRecords = data
29+
this.records = [...data]
30+
this.parentDb = parent
3031
}
3132

3233
/**
33-
* Gets the count of filtered records.
34-
* @description Returns the number of records matching current filter conditions.
34+
* Count filtered records
35+
* @description Returns records matching current conditions.
3536
* @returns Number of matching records
3637
*/
3738
count(): number {
38-
return this.data.length
39+
return this.records.length
3940
}
4041

4142
/**
42-
* Deletes all filtered records.
43-
* @description Removes all records matching current conditions from original data.
43+
* Delete filtered records
44+
* @description Removes matches from original records.
4445
* @returns Number of records deleted
4546
*/
4647
delete(): number {
47-
const deletedCount: number = this.data.length
48-
const itemsToDelete: Set<Record<string, unknown>> = new Set(this.data)
49-
for (let i: number = this.originalData.length - 1; i >= 0; i--) {
50-
if (itemsToDelete.has(this.originalData[i] as Record<string, unknown>)) {
51-
this.originalData.splice(i, 1)
48+
const deletedCount: number = this.records.length
49+
const recordsToDelete: Set<Record<string, unknown>> = new Set(this.records)
50+
for (let i: number = this.originalRecords.length - 1; i >= 0; i--) {
51+
if (recordsToDelete.has(this.originalRecords[i] as Record<string, unknown>)) {
52+
this.originalRecords.splice(i, 1)
5253
}
5354
}
54-
this.data.length = 0
55-
this.parent?.syncFromQueryBuilder(this.originalData)
55+
this.records.length = 0
56+
this.parentDb?.syncFromQueryBuilder(this.originalRecords)
5657
return deletedCount
5758
}
5859

5960
/**
60-
* Gets the first filtered record.
61-
* @description Returns the first record matching current conditions or null if none found.
61+
* Get first filtered record
62+
* @description Returns first match, or null.
6263
* @returns First matching record or null
6364
*/
6465
first(): Record<string, unknown> | null {
65-
return this.data[0] ?? null
66+
return this.records[0] ?? null
6667
}
6768

6869
/**
69-
* Gets all filtered records.
70-
* @description Returns a copy of all records matching current conditions.
70+
* Get filtered records
71+
* @description Returns copy of matching records.
7172
* @returns Array of matching records
7273
*/
7374
get(): Record<string, unknown>[] {
74-
return [...this.data]
75+
return [...this.records]
7576
}
7677

7778
/**
78-
* Updates all filtered records.
79-
* @description Applies the provided data to all records matching current conditions.
79+
* Update filtered records
80+
* @description Applies fields to all matches.
8081
* @param data - Object containing fields to update
8182
*/
8283
update(data: Record<string, unknown>): void {
83-
this.data.forEach((item: Record<string, unknown>) => {
84+
this.records.forEach((item: Record<string, unknown>) => {
8485
Object.keys(data).forEach((key: string) => {
8586
if (key.includes('.')) {
8687
this.setNestedValue(item, key, data[key])
@@ -89,42 +90,45 @@ export class QueryBuilder {
8990
}
9091
})
9192
})
92-
this.parent?.syncFromQueryBuilder(this.originalData)
93+
this.parentDb?.syncFromQueryBuilder(this.originalRecords)
9394
}
9495

9596
/**
96-
* Adds a filter condition to the query.
97-
* @description Filters records based on string condition or function predicate.
97+
* Add filter condition
98+
* @description Filters by string or predicate.
9899
* @param condition - Query string or function to filter records
99100
* @returns QueryBuilder instance for chaining
100101
*/
101102
where(condition: string | ((item: Record<string, unknown>) => boolean)): QueryBuilder {
102103
if (typeof condition === 'function') {
103-
this.data = this.data.filter(condition)
104+
this.records = this.records.filter(condition)
104105
} else {
105-
const parsed: JsonaryCondition | null = this.parseCondition(condition)
106-
if (parsed) {
107-
this.conditions.push(parsed)
108-
this.data = this.data.filter((item: Record<string, unknown>) =>
109-
this.evaluateCondition(item, parsed)
106+
const parsedCondition: Types.JsonaryCondition | null = this.parseCondition(condition)
107+
if (parsedCondition) {
108+
this.conditions.push(parsedCondition)
109+
this.records = this.records.filter((item: Record<string, unknown>) =>
110+
this.evaluateCondition(item, parsedCondition)
110111
)
111112
}
112113
}
113114
return this
114115
}
115116

116117
/**
117-
* Evaluates a condition against a data item.
118-
* @description Checks if an item matches the specified condition using appropriate operator.
118+
* Evaluate condition against item
119+
* @description Checks item match for given operator.
119120
* @param item - Data item to evaluate
120121
* @param condition - Condition to check against
121122
* @returns True if condition matches, false otherwise
122123
*/
123-
private evaluateCondition(item: Record<string, unknown>, condition: JsonaryCondition): boolean {
124-
const { operator, value }: JsonaryCondition = condition
124+
private evaluateCondition(
125+
item: Record<string, unknown>,
126+
condition: Types.JsonaryCondition
127+
): boolean {
128+
const { operator, value }: Types.JsonaryCondition = condition
125129
const fieldValue: unknown = this.getNestedValue(item, condition.field)
126-
const op: JsonaryCondition['operator'] = operator
127-
switch (op) {
130+
const operatorToken: Types.JsonaryCondition['operator'] = operator
131+
switch (operatorToken) {
128132
case queryOperators.eq:
129133
return fieldValue === value
130134
case queryOperators.neq:
@@ -157,8 +161,8 @@ export class QueryBuilder {
157161
}
158162

159163
/**
160-
* Gets a nested value from an object using dot notation.
161-
* @description Traverses object properties using dot-separated path.
164+
* Get nested value by path
165+
* @description Traverses dot-separated property path.
162166
* @param obj - Object to traverse
163167
* @param path - Dot-separated path to the property
164168
* @returns Value at the specified path or undefined
@@ -167,53 +171,53 @@ export class QueryBuilder {
167171
if (!path.includes('.')) {
168172
return obj[path]
169173
}
170-
let current: unknown = obj
174+
let currentValue: unknown = obj
171175
const keys: string[] = path.split('.')
172176
for (let i: number = 0; i < keys.length; i++) {
173-
if (current === null || current === undefined || typeof current !== 'object') {
177+
if (currentValue === null || currentValue === undefined || typeof currentValue !== 'object') {
174178
return undefined
175179
}
176180
const key: string | undefined = keys[i]
177181
if (key === undefined) {
178182
return undefined
179183
}
180-
current = (current as Record<string, unknown>)[key]
184+
currentValue = (currentValue as Record<string, unknown>)[key]
181185
}
182-
return current
186+
return currentValue
183187
}
184188

185189
/**
186-
* Parses a string condition into structured format.
187-
* @description Converts string-based conditions into JsonaryCondition objects.
190+
* Parse condition string
191+
* @description Converts string to condition object.
188192
* @param condition - String condition to parse
189193
* @returns Parsed condition object or null if invalid
190194
*/
191-
private parseCondition(condition: string): JsonaryCondition | null {
192-
if (QueryBuilder.conditionRecent.has(condition)) {
193-
return QueryBuilder.conditionRecent.get(condition) ?? null
195+
private parseCondition(condition: string): Types.JsonaryCondition | null {
196+
if (QueryBuilder.recentConditionCache.has(condition)) {
197+
return QueryBuilder.recentConditionCache.get(condition) ?? null
194198
}
195199
const operators: string[] = getOperatorsSorted()
196-
for (const op of operators) {
197-
const index: number = condition.indexOf(op)
200+
for (const operatorToken of operators) {
201+
const index: number = condition.indexOf(operatorToken)
198202
if (index !== -1) {
199-
const value: string = condition.substring(index + op.length).trim()
200-
const parsedValue: unknown = this.parseSpecialValue(this.stripQuotes(value))
201-
const result: JsonaryCondition = {
203+
const rawValue: string = condition.substring(index + operatorToken.length).trim()
204+
const parsedValue: unknown = this.parseSpecialValue(this.stripQuotes(rawValue))
205+
const result: Types.JsonaryCondition = {
202206
field: condition.substring(0, index).trim(),
203-
operator: op as JsonaryCondition['operator'],
207+
operator: operatorToken as Types.JsonaryCondition['operator'],
204208
value: parsedValue
205209
}
206-
QueryBuilder.conditionRecent.set(condition, result)
210+
QueryBuilder.recentConditionCache.set(condition, result)
207211
return result
208212
}
209213
}
210-
QueryBuilder.conditionRecent.set(condition, null)
214+
QueryBuilder.recentConditionCache.set(condition, null)
211215
return null
212216
}
213217

214218
/**
215-
* Parses special string values to their appropriate types.
216-
* @description Converts string representations of special values to their actual types.
219+
* Parse special value tokens
220+
* @description Converts tokens to typed values.
217221
* @param value - Value to parse
218222
* @returns Parsed value in appropriate type
219223
*/
@@ -236,34 +240,38 @@ export class QueryBuilder {
236240
}
237241

238242
/**
239-
* Sets a nested value in an object using dot notation.
240-
* @description Creates nested objects as needed and sets the final value.
243+
* Set nested value by path
244+
* @description Creates objects and sets final value.
241245
* @param obj - Object to modify
242246
* @param path - Dot-separated path to the property
243247
* @param value - Value to set
244248
*/
245249
private setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
246250
const keys: string[] = path.split('.')
247-
let current: Record<string, unknown> = obj
251+
let currentObject: Record<string, unknown> = obj
248252
for (let i: number = 0; i < keys.length - 1; i++) {
249253
const key: string | undefined = keys[i]
250254
if (key === undefined) {
251255
continue
252256
}
253-
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
254-
current[key] = {}
257+
if (
258+
!(key in currentObject) ||
259+
typeof currentObject[key] !== 'object' ||
260+
currentObject[key] === null
261+
) {
262+
currentObject[key] = {}
255263
}
256-
current = current[key] as Record<string, unknown>
264+
currentObject = currentObject[key] as Record<string, unknown>
257265
}
258266
const lastKey: string | undefined = keys[keys.length - 1]
259267
if (lastKey !== undefined) {
260-
current[lastKey] = value
268+
currentObject[lastKey] = value
261269
}
262270
}
263271

264272
/**
265-
* Strips quotes from string values.
266-
* @description Removes surrounding single or double quotes from string values.
273+
* Strip surrounding quotes
274+
* @description Removes wrapping single or double quotes.
267275
* @param value - String value to process
268276
* @returns Processed value with quotes removed if applicable
269277
*/

0 commit comments

Comments
 (0)