@codeleap/debug 5.8.8

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/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@codeleap/debug",
3
+ "version": "5.8.8",
4
+ "main": "src/index.ts",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
8
+ "type": "git",
9
+ "directory": "packages/debug"
10
+ },
11
+ "devDependencies": {
12
+ "@codeleap/config": "5.8.8",
13
+ "ts-node-dev": "1.1.8"
14
+ },
15
+ "scripts": {
16
+ "build": "echo 'No build needed'"
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "5.5.2"
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@codeleap/debug",
3
+ "version": "5.8.8",
4
+ "main": "src/index.ts",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
8
+ "type": "git",
9
+ "directory": "packages/debug"
10
+ },
11
+ "devDependencies": {
12
+ "@codeleap/config": "workspace:*",
13
+ "ts-node-dev": "1.1.8"
14
+ },
15
+ "scripts": {
16
+ "build": "echo 'No build needed'"
17
+ },
18
+ "peerDependencies": {
19
+ "typescript": "5.5.2"
20
+ }
21
+ }
@@ -0,0 +1,243 @@
1
+ import { FakeItem, FakeRestApiOptions, PaginationResponse } from './types'
2
+
3
+ /**
4
+ * A fake REST API implementation for testing and prototyping.
5
+ * Provides CRUD operations with pagination support and optional request delays.
6
+ * @template T - The type of items to manage, must extend FakeItem
7
+ * @template F - The type of filters to apply when listing items
8
+ */
9
+ export class FakeRestApi<T extends FakeItem, F = Record<string, unknown>> {
10
+ private data: T[] = []
11
+ private options: Required<FakeRestApiOptions<T, F>>
12
+
13
+ /**
14
+ * Gets the ID of the last item in the data array.
15
+ * @private
16
+ * @returns The highest ID or 0 if no items exist
17
+ */
18
+ private get lastId(): T['id'] {
19
+ return this.data.length > 0 ? Math.max(...this.data.map((i) => i.id)) : 0
20
+ }
21
+
22
+ /**
23
+ * Gets the name of the API.
24
+ * @returns The API name
25
+ */
26
+ public get name(): string {
27
+ return this.options.name
28
+ }
29
+
30
+ /**
31
+ * Gets the current count of items.
32
+ * @returns The number of items in the data array
33
+ */
34
+ public get count(): number {
35
+ return this.data.length
36
+ }
37
+
38
+ /**
39
+ * Creates a new FakeRestApi instance.
40
+ * @param options - Configuration options for the API
41
+ */
42
+ constructor(options: FakeRestApiOptions<T, F>) {
43
+ this.options = {
44
+ delayMs: 2500,
45
+ maxCount: 100,
46
+ enableDelay: false,
47
+ filterFn: () => true,
48
+ ...options,
49
+ }
50
+
51
+ this.initialize()
52
+ }
53
+
54
+ /**
55
+ * Initializes the data array with items up to maxCount.
56
+ * @private
57
+ */
58
+ private initialize(): void {
59
+ this.data = Array.from({ length: this.options.maxCount }, (_, i) =>
60
+ this.generateItem(i + 1)
61
+ )
62
+ }
63
+
64
+ /**
65
+ * Updates the API options partially.
66
+ * @param newOptions - Partial options to update
67
+ */
68
+ public setOptions(newOptions: Partial<FakeRestApiOptions<T, F>>): void {
69
+ this.options = { ...this.options, ...newOptions }
70
+ }
71
+
72
+ /**
73
+ * Generates a new item using the generator function.
74
+ * @param id - The ID for the new item (defaults to lastId + 1)
75
+ * @returns A newly generated item
76
+ */
77
+ public generateItem(id: T['id'] = this.lastId + 1): T {
78
+ return this.options.generatorFn(id)
79
+ }
80
+
81
+ /**
82
+ * Introduces an artificial delay if enabled in options.
83
+ * @private
84
+ * @returns A promise that resolves after the delay period
85
+ */
86
+ private async delay(): Promise<void> {
87
+ if (!this.options.enableDelay) return
88
+
89
+ return new Promise((resolve) => {
90
+ setTimeout(resolve, this.options.delayMs)
91
+ })
92
+ }
93
+
94
+ /**
95
+ * Builds a URL with pagination parameters.
96
+ * @private
97
+ * @param limit - Number of items per page
98
+ * @param offset - Starting position
99
+ * @returns A formatted URL string
100
+ */
101
+ private buildUrl(limit: number, offset: number): string {
102
+ return `https://api.${this.name}?limit=${limit}&offset=${offset}`
103
+ }
104
+
105
+ /**
106
+ * Retrieves a paginated list of items with optional filtering.
107
+ * @param limit - Number of items to return (default: 10)
108
+ * @param offset - Starting position (default: 0)
109
+ * @param filters - Optional filters to apply
110
+ * @returns A promise that resolves to a paginated response
111
+ */
112
+ async listItems(
113
+ limit = 10,
114
+ offset = 0,
115
+ filters?: F
116
+ ): Promise<PaginationResponse<T>> {
117
+ await this.delay()
118
+
119
+ const hasFilters = filters && Object.keys(filters).length > 0
120
+ const items = hasFilters
121
+ ? this.data.filter((item) => this.options.filterFn(item, filters))
122
+ : this.data
123
+
124
+ const total = items.length
125
+ const start = Math.max(0, offset)
126
+ const end = Math.min(start + limit, total)
127
+
128
+ const results = items.slice(start, end)
129
+
130
+ return {
131
+ count: total,
132
+ next: end < total ? this.buildUrl(limit, end) : null,
133
+ previous: start > 0 ? this.buildUrl(limit, Math.max(0, start - limit)) : null,
134
+ results,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Retrieves a single item by ID.
140
+ * @param id - The ID of the item to retrieve
141
+ * @returns A promise that resolves to the requested item
142
+ * @throws {Error} If the item is not found
143
+ */
144
+ async retrieveItem(id: T['id']): Promise<T> {
145
+ await this.delay()
146
+
147
+ const item = this.data.find((i) => i.id === id)
148
+
149
+ if (!item) {
150
+ throw new Error(`${this.name} with id ${id} not found`)
151
+ }
152
+
153
+ return item
154
+ }
155
+
156
+ /**
157
+ * Creates a new item and adds it to the data array.
158
+ * @param item - Optional partial item data (ID will be auto-generated)
159
+ * @returns A promise that resolves to the created item
160
+ */
161
+ async createItem(item?: Partial<T>): Promise<T> {
162
+ await this.delay()
163
+
164
+ const id = this.lastId + 1
165
+ const newItem = item ? { ...item, id } as T : this.generateItem(id)
166
+
167
+ this.data.push(newItem)
168
+
169
+ return newItem
170
+ }
171
+
172
+ /**
173
+ * Updates an existing item by ID.
174
+ * @param id - The ID of the item to update
175
+ * @param updates - Partial item data to update
176
+ * @returns A promise that resolves to the updated item
177
+ * @throws {Error} If the item is not found
178
+ */
179
+ async updateItem(id: T['id'], updates: Partial<T>): Promise<T> {
180
+ await this.delay()
181
+
182
+ const index = this.data.findIndex((i) => i.id === id)
183
+
184
+ if (index === -1) {
185
+ throw new Error(`${this.name} with id ${id} not found`)
186
+ }
187
+
188
+ this.data[index] = { ...this.data[index], ...updates, id }
189
+
190
+ return this.data[index]
191
+ }
192
+
193
+ /**
194
+ * Deletes an item by ID.
195
+ * @param id - The ID of the item to delete
196
+ * @returns A promise that resolves to the deleted item
197
+ * @throws {Error} If the item is not found
198
+ */
199
+ async deleteItem(id: T['id']): Promise<T> {
200
+ await this.delay()
201
+
202
+ const index = this.data.findIndex((i) => i.id === id)
203
+
204
+ if (index === -1) {
205
+ throw new Error(`${this.name} with id ${id} not found`)
206
+ }
207
+
208
+ const [deleted] = this.data.splice(index, 1)
209
+
210
+ return deleted
211
+ }
212
+
213
+ /**
214
+ * Resets the data array to its initial state by regenerating all items.
215
+ */
216
+ public reset(): void {
217
+ this.initialize()
218
+ }
219
+
220
+ /**
221
+ * Clears all items from the data array.
222
+ */
223
+ public clear(): void {
224
+ this.data = []
225
+ }
226
+
227
+ /**
228
+ * Gets a readonly copy of all items in the data array.
229
+ * @returns A frozen array of all items
230
+ */
231
+ public getData(): readonly T[] {
232
+ return Object.freeze([...this.data])
233
+ }
234
+
235
+ /**
236
+ * Gets a single item by ID without delay.
237
+ * @param id - The ID of the item to retrieve
238
+ * @returns The item if found, undefined otherwise
239
+ */
240
+ public getItem(id: T['id']): T | undefined {
241
+ return this.data.find((i) => i.id === id)
242
+ }
243
+ }
@@ -0,0 +1,29 @@
1
+ import { FakeRestApi } from './class'
2
+ import { FakeItem, FakeRestApiOptions } from './types'
3
+
4
+ /**
5
+ * Factory function to create a new FakeRestApi instance.
6
+ * @template T - The type of items to manage, must extend FakeItem
7
+ * @template F - The type of filters to apply when listing items
8
+ * @param options - Configuration options for the API
9
+ * @returns A new FakeRestApi instance
10
+ * @example
11
+ * ```typescript
12
+ * type User = { id: number; name: string; email: string }
13
+ *
14
+ * const api = createFakeRestApi<User>({
15
+ * name: 'users',
16
+ * maxCount: 50,
17
+ * generatorFn: (id) => ({
18
+ * id,
19
+ * name: `User ${id}`,
20
+ * email: `user${id}@example.com`
21
+ * })
22
+ * })
23
+ * ```
24
+ */
25
+ export function createFakeRestApi<T extends FakeItem, F = Record<string, unknown>>(
26
+ options: FakeRestApiOptions<T, F>
27
+ ): FakeRestApi<T, F> {
28
+ return new FakeRestApi<T, F>(options)
29
+ }
@@ -0,0 +1,3 @@
1
+ export * from './factor'
2
+ export * from './class'
3
+ export type { FakeRestApiOptions } from './types'
@@ -0,0 +1,368 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test'
2
+ import { FakeRestApi } from './class'
3
+ import { createFakeRestApi } from './factor'
4
+
5
+ type User = {
6
+ id: number
7
+ name: string
8
+ email: string
9
+ age: number
10
+ active: boolean
11
+ }
12
+
13
+ type UserFilters = {
14
+ name?: string
15
+ minAge?: number
16
+ active?: boolean
17
+ }
18
+
19
+ describe('FakeRestApi', () => {
20
+ let api: FakeRestApi<User, UserFilters>
21
+
22
+ beforeEach(() => {
23
+ api = createFakeRestApi<User, UserFilters>({
24
+ name: 'users',
25
+ enableDelay: false,
26
+ maxCount: 20,
27
+ generatorFn: (id) => ({
28
+ id,
29
+ name: `User ${id}`,
30
+ email: `user${id}@example.com`,
31
+ age: 20 + id,
32
+ active: id % 2 === 0,
33
+ }),
34
+ filterFn: (item, filters) => {
35
+ if (filters.name && !item.name.toLowerCase().includes(filters.name.toLowerCase())) {
36
+ return false
37
+ }
38
+
39
+ if (filters.minAge !== undefined && item.age < filters.minAge) {
40
+ return false
41
+ }
42
+
43
+ if (filters.active !== undefined && item.active !== filters.active) {
44
+ return false
45
+ }
46
+
47
+ return true
48
+ },
49
+ })
50
+ })
51
+
52
+ describe('Constructor', () => {
53
+ test('should create the API with default settings', () => {
54
+ expect(api.name).toBe('users')
55
+ expect(api.count).toBe(20)
56
+ })
57
+
58
+ test('should automatically generate initial items', () => {
59
+ const data = api.getData()
60
+ expect(data).toHaveLength(20)
61
+ expect(data[0].id).toBe(1)
62
+ expect(data[0].name).toBe('User 1')
63
+ })
64
+ })
65
+
66
+ describe('listItems', () => {
67
+ test('should list items with default pagination', async () => {
68
+ const result = await api.listItems()
69
+
70
+ expect(result.count).toBe(20)
71
+ expect(result.results).toHaveLength(10)
72
+ expect(result.next).toBe('https://api.users?limit=10&offset=10')
73
+ expect(result.previous).toBeNull()
74
+ })
75
+
76
+ test('should respect limit and offset', async () => {
77
+ const result = await api.listItems(5, 10)
78
+
79
+ expect(result.results).toHaveLength(5)
80
+ expect(result.results[0].id).toBe(11)
81
+ expect(result.next).toBe('https://api.users?limit=5&offset=15')
82
+ expect(result.previous).toBe('https://api.users?limit=5&offset=5')
83
+ })
84
+
85
+ test('should return null for next when there are no more items', async () => {
86
+ const result = await api.listItems(10, 15)
87
+
88
+ expect(result.results).toHaveLength(5)
89
+ expect(result.next).toBeNull()
90
+ })
91
+
92
+ test('should return null for previous when offset is 0', async () => {
93
+ const result = await api.listItems(10, 0)
94
+
95
+ expect(result.previous).toBeNull()
96
+ })
97
+
98
+ test('should filter by name', async () => {
99
+ const result = await api.listItems(20, 0, { name: 'User 1' })
100
+
101
+ expect(result.count).toBe(11)
102
+ expect(result.results.every(u => u.name.includes('User 1'))).toBe(true)
103
+ })
104
+
105
+ test('should filter by minimum age', async () => {
106
+ const result = await api.listItems(20, 0, { minAge: 30 })
107
+
108
+ expect(result.results.every(u => u.age >= 30)).toBe(true)
109
+ })
110
+
111
+ test('should filter by active status', async () => {
112
+ const result = await api.listItems(20, 0, { active: true })
113
+
114
+ expect(result.results.every(u => u.active === true)).toBe(true)
115
+ })
116
+
117
+ test('should apply multiple filters', async () => {
118
+ const result = await api.listItems(20, 0, { active: true, minAge: 25 })
119
+
120
+ expect(result.results.every(u => u.active === true && u.age >= 25)).toBe(true)
121
+ })
122
+
123
+ test('should handle negative offset', async () => {
124
+ const result = await api.listItems(10, -5)
125
+
126
+ expect(result.results[0].id).toBe(1)
127
+ })
128
+ })
129
+
130
+ describe('retrieveItem', () => {
131
+ test('should retrieve item by id', async () => {
132
+ const user = await api.retrieveItem(5)
133
+
134
+ expect(user.id).toBe(5)
135
+ expect(user.name).toBe('User 5')
136
+ expect(user.email).toBe('user5@example.com')
137
+ })
138
+
139
+ test('should throw an error when item does not exist', async () => {
140
+ expect(async () => {
141
+ await api.retrieveItem(999)
142
+ }).toThrow('users with id 999 not found')
143
+ })
144
+ })
145
+
146
+ describe('createItem', () => {
147
+ test('should create item with provided data', async () => {
148
+ const newUser = await api.createItem({
149
+ name: 'John Smith',
150
+ email: 'john@example.com',
151
+ age: 28,
152
+ active: true,
153
+ })
154
+
155
+ expect(newUser.id).toBe(21)
156
+ expect(newUser.name).toBe('John Smith')
157
+ expect(api.count).toBe(21)
158
+ })
159
+
160
+ test('should generate item automatically when not provided', async () => {
161
+ const newUser = await api.createItem()
162
+
163
+ expect(newUser.id).toBe(21)
164
+ expect(newUser.name).toBe('User 21')
165
+ expect(api.count).toBe(21)
166
+ })
167
+
168
+ test('should auto-increment the id', async () => {
169
+ const user1 = await api.createItem()
170
+ const user2 = await api.createItem()
171
+
172
+ expect(user1.id).toBe(21)
173
+ expect(user2.id).toBe(22)
174
+ })
175
+ })
176
+
177
+ describe('updateItem', () => {
178
+ test('should update existing item', async () => {
179
+ const updated = await api.updateItem(5, {
180
+ name: 'User Updated',
181
+ age: 99,
182
+ })
183
+
184
+ expect(updated.id).toBe(5)
185
+ expect(updated.name).toBe('User Updated')
186
+ expect(updated.age).toBe(99)
187
+ expect(updated.email).toBe('user5@example.com')
188
+ })
189
+
190
+ test('should preserve the original id', async () => {
191
+ const updated = await api.updateItem(5, { id: 999 } as any)
192
+
193
+ expect(updated.id).toBe(5)
194
+ })
195
+
196
+ test('should throw an error when item does not exist', async () => {
197
+ expect(async () => {
198
+ await api.updateItem(999, { name: 'Test' })
199
+ }).toThrow('users with id 999 not found')
200
+ })
201
+ })
202
+
203
+ describe('deleteItem', () => {
204
+ test('should delete existing item', async () => {
205
+ const deleted = await api.deleteItem(5)
206
+
207
+ expect(deleted.id).toBe(5)
208
+ expect(api.count).toBe(19)
209
+ expect(api.getItem(5)).toBeUndefined()
210
+ })
211
+
212
+ test('should throw an error when item does not exist', async () => {
213
+ expect(async () => {
214
+ await api.deleteItem(999)
215
+ }).toThrow('users with id 999 not found')
216
+ })
217
+ })
218
+
219
+ describe('Utility methods', () => {
220
+ test('getItem should return existing item', () => {
221
+ const user = api.getItem(5)
222
+
223
+ expect(user).toBeDefined()
224
+ expect(user?.id).toBe(5)
225
+ })
226
+
227
+ test('getItem should return undefined for non-existing item', () => {
228
+ const user = api.getItem(999)
229
+
230
+ expect(user).toBeUndefined()
231
+ })
232
+
233
+ test('getData should return frozen array', () => {
234
+ const data = api.getData()
235
+
236
+ expect(data).toHaveLength(20)
237
+ expect(Object.isFrozen(data)).toBe(true)
238
+ })
239
+
240
+ test('clear should remove all items', () => {
241
+ api.clear()
242
+
243
+ expect(api.count).toBe(0)
244
+ expect(api.getData()).toHaveLength(0)
245
+ })
246
+
247
+ test('reset should restore initial items', () => {
248
+ api.clear()
249
+ expect(api.count).toBe(0)
250
+
251
+ api.reset()
252
+
253
+ expect(api.count).toBe(20)
254
+ expect(api.getData()[0].id).toBe(1)
255
+ })
256
+
257
+ test('generateItem should generate new item with provided id', () => {
258
+ const user = api.generateItem(100)
259
+
260
+ expect(user.id).toBe(100)
261
+ expect(user.name).toBe('User 100')
262
+ })
263
+
264
+ test('generateItem should use lastId + 1 when not provided', () => {
265
+ const user = api.generateItem()
266
+
267
+ expect(user.id).toBe(21)
268
+ })
269
+ })
270
+
271
+ describe('setOptions', () => {
272
+ test('should update options', () => {
273
+ api.setOptions({ enableDelay: true, delayMs: 5000 })
274
+
275
+ expect(api.name).toBe('users')
276
+ })
277
+ })
278
+
279
+ describe('Delay', () => {
280
+ test('should respect delay when enabled', async () => {
281
+ const apiWithDelay = createFakeRestApi<User>({
282
+ name: 'delayed',
283
+ enableDelay: true,
284
+ delayMs: 100,
285
+ maxCount: 5,
286
+ generatorFn: (id) => ({
287
+ id,
288
+ name: `User ${id}`,
289
+ email: `user${id}@example.com`,
290
+ age: 20,
291
+ active: true,
292
+ }),
293
+ })
294
+
295
+ const start = Date.now()
296
+ await apiWithDelay.listItems()
297
+ const duration = Date.now() - start
298
+
299
+ expect(duration).toBeGreaterThanOrEqual(100)
300
+ })
301
+
302
+ test('should be instantaneous when delay is disabled', async () => {
303
+ const start = Date.now()
304
+ await api.listItems()
305
+ const duration = Date.now() - start
306
+
307
+ expect(duration).toBeLessThan(50)
308
+ })
309
+ })
310
+
311
+ describe('Edge cases', () => {
312
+ test('should handle maxCount = 0', () => {
313
+ const emptyApi = createFakeRestApi<User>({
314
+ name: 'empty',
315
+ maxCount: 0,
316
+ generatorFn: (id) => ({
317
+ id,
318
+ name: `User ${id}`,
319
+ email: `user${id}@example.com`,
320
+ age: 20,
321
+ active: true,
322
+ }),
323
+ })
324
+
325
+ expect(emptyApi.count).toBe(0)
326
+ })
327
+
328
+ test('should handle empty filters', async () => {
329
+ const result = await api.listItems(10, 0, {})
330
+
331
+ expect(result.count).toBe(20)
332
+ })
333
+
334
+ test('should handle limit greater than total', async () => {
335
+ const result = await api.listItems(100, 0)
336
+
337
+ expect(result.results).toHaveLength(20)
338
+ expect(result.next).toBeNull()
339
+ })
340
+
341
+ test('should handle offset greater than total', async () => {
342
+ const result = await api.listItems(10, 100)
343
+
344
+ expect(result.results).toHaveLength(0)
345
+ expect(result.next).toBeNull()
346
+ })
347
+ })
348
+
349
+ describe('API without filterFn', () => {
350
+ test('should work without filterFn defined', async () => {
351
+ const simpleApi = createFakeRestApi<User>({
352
+ name: 'simple',
353
+ maxCount: 10,
354
+ generatorFn: (id) => ({
355
+ id,
356
+ name: `User ${id}`,
357
+ email: `user${id}@example.com`,
358
+ age: 20,
359
+ active: true,
360
+ }),
361
+ })
362
+
363
+ const result = await simpleApi.listItems(5, 0, { name: 'test' })
364
+
365
+ expect(result.count).toBe(10)
366
+ })
367
+ })
368
+ })
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Represents a basic item with an ID.
3
+ */
4
+ export type FakeItem = {
5
+ id: number
6
+ }
7
+
8
+ /**
9
+ * Represents a paginated response structure.
10
+ * @template T - The type of items in the results array, must extend FakeItem
11
+ */
12
+ export type PaginationResponse<T extends FakeItem> = {
13
+ /** Total count of items */
14
+ count: number
15
+ /** URL for the next page, or null if there are no more pages */
16
+ next: string | null
17
+ /** URL for the previous page, or null if this is the first page */
18
+ previous: string | null
19
+ /** Array of items for the current page */
20
+ results: T[]
21
+ }
22
+
23
+ /**
24
+ * Configuration options for creating a FakeRestApi instance.
25
+ * @template T - The type of items to manage, must extend FakeItem
26
+ * @template F - The type of filters to apply when listing items
27
+ */
28
+ export type FakeRestApiOptions<T extends FakeItem, F = Record<string, unknown>> = {
29
+ /** Name of the API (used in URLs and error messages) */
30
+ name: string
31
+ /** Whether to enable artificial delay for requests */
32
+ enableDelay?: boolean
33
+ /** Maximum number of items to generate on initialization */
34
+ maxCount?: number
35
+ /** Delay duration in milliseconds */
36
+ delayMs?: number
37
+ /** Function to generate a new item given an ID */
38
+ generatorFn: (id: T['id']) => T
39
+ /** Optional function to filter items based on criteria */
40
+ filterFn?: (item: T, filters: F) => boolean
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './FakeRestApi'