Skip to content

Commit da9ffa4

Browse files
committed
feat: add paginator, serlaize helper and revamp API
1 parent 1299039 commit da9ffa4

File tree

22 files changed

+943
-544
lines changed

22 files changed

+943
-544
lines changed

benchmarks/recursive_promises.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-ignore
22
import Benchmark from 'benchmark'
3-
import { type ResourceDataTypes } from '../src/types.js'
3+
import { type ResourceDataTypes } from '../src/types.ts'
44
const suite = new Benchmark.Suite()
55

66
function lazy<T>(resolver: T): T {

index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
* file that was distributed with this source code.
88
*/
99

10-
export { Item } from './src/item.js'
11-
export { Maybe } from './src/maybe.js'
12-
export { transform } from './src/transform.js'
13-
export { Collection } from './src/collection.js'
14-
export { BaseTransformer } from './src/base_transformer.js'
10+
export { Maybe } from './src/maybe.ts'
11+
export { Item } from './src/resource/item.ts'
12+
export { serialize } from './src/serialize.ts'
13+
export { Collection } from './src/resource/collection.ts'
14+
export { BaseTransformer } from './src/base_transformer.ts'

src/base_transformer.ts

Lines changed: 108 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99

1010
import { RuntimeException } from '@poppinss/exception'
1111

12-
import { Item } from './item.js'
13-
import { Maybe } from './maybe.js'
14-
import { Collection } from './collection.js'
12+
import { Maybe } from './maybe.ts'
13+
import { Item } from './resource/item.ts'
14+
import { Collection } from './resource/collection.ts'
15+
import { Paginator } from './paginator.ts'
1516

1617
/**
1718
* Serves as the base for creating custom data transformers.
@@ -37,35 +38,102 @@ import { Collection } from './collection.js'
3738
*/
3839
export abstract class BaseTransformer<T> {
3940
/**
40-
* Specify a one-to-one relationship with a given transformer.
41+
* Static method to transform data into Item or Collection instances.
42+
* Handles single objects, arrays, null values, and Maybe-wrapped values.
4143
*
42-
* @param data - The data to transform, can be wrapped in Maybe for optional values
44+
* @param data - The data to transform (can be single object, array, null, or Maybe-wrapped)
4345
*
4446
* @example
4547
* ```ts
4648
* class UserTransformer extends BaseTransformer<User> {
4749
* toObject() {
48-
* return {
49-
* id: this.resource.id,
50-
* profile: UserTransformer.item(this.resource.profile)
51-
* }
50+
* return { id: this.resource.id, name: this.resource.name }
5251
* }
5352
* }
53+
*
54+
* // Transform single user
55+
* const userItem = UserTransformer.transform(user)
56+
*
57+
* // Transform array of users
58+
* const userCollection = UserTransformer.transform([user1, user2])
59+
*
60+
* // Transform with Maybe wrapper
61+
* const maybeUser = UserTransformer.transform(new Maybe(user))
5462
* ```
5563
*/
56-
static item<Self extends { new (...args: any[]): any }>(
64+
/**
65+
* Transform data wrapped in Maybe that can be undefined
66+
*
67+
* @param data - Maybe-wrapped data that can be undefined
68+
*/
69+
static transform<Self extends { new (...args: any[]): any }>(
70+
this: Self,
71+
data: Maybe<ConstructorParameters<Self>[0]>
72+
): Item<InstanceType<Self>, 1, 'toObject'> | undefined
73+
74+
/**
75+
* Transform a single data object
76+
*
77+
* @param data - Single data object to transform
78+
*/
79+
static transform<Self extends { new (...args: any[]): any }>(
80+
this: Self,
81+
data: ConstructorParameters<Self>[0]
82+
): Item<InstanceType<Self>, 1, 'toObject'>
83+
84+
/**
85+
* Transform data wrapped in Maybe that can be undefined or null
86+
*
87+
* @param data - Maybe-wrapped data that can be undefined or null
88+
*/
89+
static transform<Self extends { new (...args: any[]): any }>(
5790
this: Self,
5891
data: Maybe<ConstructorParameters<Self>[0] | null>
59-
): Item<InstanceType<Self>, 1, 'toObject', null> | undefined
92+
): Item<InstanceType<Self>, 1, 'toObject'> | undefined | null
6093

61-
static item<Self extends { new (...args: any[]): any }>(
94+
/**
95+
* Transform a single data object that can be null
96+
*
97+
* @param data - Single data object that can be null
98+
*/
99+
static transform<Self extends { new (...args: any[]): any }>(
62100
this: Self,
63101
data: ConstructorParameters<Self>[0] | null
64-
): Item<InstanceType<Self>, 1, 'toObject', null>
102+
): Item<InstanceType<Self>, 1, 'toObject'> | null
65103

66-
static item<Self extends { new (...args: any[]): any }>(
104+
/**
105+
* Transform an array wrapped in Maybe that can be undefined
106+
*
107+
* @param data - Maybe-wrapped array that can be undefined
108+
*/
109+
static transform<Self extends { new (...args: any[]): any }>(
67110
this: Self,
68-
data: ConstructorParameters<Self>[0] | null | Maybe<ConstructorParameters<Self>[0] | null>
111+
data: Maybe<ConstructorParameters<Self>[0][]>
112+
): Collection<InstanceType<Self>, 1, 'toObject'> | undefined
113+
114+
/**
115+
* Transform an array of data objects
116+
*
117+
* @param data - Array of data objects to transform
118+
*/
119+
static transform<Self extends { new (...args: any[]): any }>(
120+
this: Self,
121+
data: ConstructorParameters<Self>[0][]
122+
): Collection<InstanceType<Self>, 1, 'toObject'>
123+
124+
/**
125+
* Implementation method that handles all transform overloads
126+
*
127+
* @param data - Union of all possible data types that can be transformed
128+
*/
129+
static transform<Self extends { new (...args: any[]): any }>(
130+
this: Self,
131+
data:
132+
| ConstructorParameters<Self>[0]
133+
| null
134+
| Maybe<ConstructorParameters<Self>[0] | null>
135+
| ConstructorParameters<Self>[0][]
136+
| Maybe<ConstructorParameters<Self>[0][]>
69137
) {
70138
/**
71139
* If optional values are allowed and the value is undefined, then
@@ -77,58 +145,50 @@ export abstract class BaseTransformer<T> {
77145
return undefined
78146
}
79147

80-
return new Item<InstanceType<Self>, 1, 'toObject', null>(
148+
if (Array.isArray(unwrappedValue)) {
149+
return new Collection(unwrappedValue, this, 1, 'toObject', new RuntimeException())
150+
}
151+
152+
if (unwrappedValue === null) {
153+
return null
154+
}
155+
156+
return new Item<InstanceType<Self>, 1, 'toObject'>(
81157
unwrappedValue,
82158
this,
83159
1,
84160
'toObject',
85-
new RuntimeException(),
86-
true
161+
new RuntimeException()
87162
)
88163
}
89164

90165
/**
91-
* Specify a many relationship with a given transformer.
166+
* Create a paginated collection from an array of data
92167
*
93-
* @param data - Array of data to transform, can be wrapped in Maybe for optional values
168+
* @param data - Array of data objects to transform into a paginated collection
94169
*
95170
* @example
96171
* ```ts
97172
* class UserTransformer extends BaseTransformer<User> {
98173
* toObject() {
99-
* return {
100-
* id: this.resource.id,
101-
* posts: PostTransformer.collection(this.resource.posts)
102-
* }
174+
* return { id: this.resource.id, name: this.resource.name }
103175
* }
104176
* }
177+
*
178+
* // Create paginated collection
179+
* const paginatedUsers = UserTransformer.paginate([user1, user2, user3])
105180
* ```
106181
*/
107-
static collection<Self extends { new (...args: any[]): any }>(
108-
this: Self,
109-
data: Maybe<ConstructorParameters<Self>[0][]>
110-
): Collection<InstanceType<Self>, 1, 'toObject'> | undefined
111-
112-
static collection<Self extends { new (...args: any[]): any }>(
182+
static paginate<Self extends { new (...args: any[]): any }, MetaData extends Record<string, any>>(
113183
this: Self,
114-
data: ConstructorParameters<Self>[0][]
115-
): Collection<InstanceType<Self>, 1, 'toObject'>
116-
117-
static collection<Self extends { new (...args: any[]): any }>(
118-
this: Self,
119-
data: ConstructorParameters<Self>[0][] | Maybe<ConstructorParameters<Self>[0][]>
120-
) {
121-
/**
122-
* If optional values are allowed and the value is undefined, then
123-
* we return undefined
124-
*/
125-
const canBeOptional = data instanceof Maybe
126-
const unwrappedValue = canBeOptional ? data.value : data
127-
if (canBeOptional && unwrappedValue === undefined) {
128-
return undefined
129-
}
130-
131-
return new Collection(unwrappedValue, this, 1, 'toObject', new RuntimeException())
184+
data: ConstructorParameters<Self>[0][],
185+
metaData: MetaData
186+
): Paginator<Collection<InstanceType<Self>, 1, 'toObject'>, 'data', MetaData> {
187+
return new Paginator(
188+
new Collection(data, this, 1, 'toObject', new RuntimeException()),
189+
'data',
190+
metaData
191+
)
132192
}
133193

134194
/**

src/debug.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,14 @@
99

1010
import { debuglog } from 'node:util'
1111

12+
/**
13+
* Debug logger instance for AdonisJS data transformation operations.
14+
* Uses Node.js built-in util.debuglog for conditional logging based on NODE_DEBUG environment variable.
15+
*
16+
* @example
17+
* ```ts
18+
* debug('Transforming user data: %o', userData)
19+
* debug('Collection processing started with %d items', items.length)
20+
* ```
21+
*/
1222
export const debug = debuglog('adonisjs:data')

src/maybe.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,10 @@ export class Maybe<T> {
3535
* const maybeUndefined = new Maybe(undefined)
3636
* ```
3737
*/
38-
constructor(public value: T) {}
38+
constructor(
39+
/**
40+
* The wrapped value, which may be undefined
41+
*/
42+
public value: T
43+
) {}
3944
}

src/paginator.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* @adonisjs/http-transformers
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import type { ContainerResolver } from '@adonisjs/fold'
11+
12+
import { type UnpackAsCollection } from './types.ts'
13+
import { type Collection } from './resource/collection.ts'
14+
15+
/**
16+
* Represents paginated data that combines a collection of transformed items
17+
* with pagination metadata.
18+
*
19+
* @template PaginatorCollection - The collection type containing the data items
20+
* @template DataProp - The property name where data items will be stored
21+
* @template MetaData - The pagination metadata type
22+
*
23+
* @example
24+
* ```ts
25+
* const paginator = new Paginator(
26+
* UserTransformer.collection(users),
27+
* 'data',
28+
* { page: 1, perPage: 10, total: 100 }
29+
* )
30+
*
31+
* const result = await paginator.serialize(container, 0)
32+
* // Result: { data: [...], page: 1, perPage: 10, total: 100 }
33+
* ```
34+
*/
35+
export class Paginator<
36+
PaginatorCollection extends Collection<any, any, any>,
37+
DataProp extends string,
38+
MetaData extends Record<string, any>,
39+
> {
40+
/**
41+
* Type identifier for the paginator
42+
*/
43+
$type: 'paginator' = 'paginator'
44+
45+
/**
46+
* Creates a new Paginator instance
47+
*
48+
* @param collection - The collection of data to paginate
49+
* @param dataProp - The property name for the data array in the result
50+
* @param metaData - Pagination metadata (page, perPage, total, etc.)
51+
*
52+
* @example
53+
* ```ts
54+
* const paginator = new Paginator(
55+
* UserTransformer.collection(users),
56+
* 'data',
57+
* { page: 1, perPage: 10, total: 100, lastPage: 10 }
58+
* )
59+
* ```
60+
*/
61+
constructor(
62+
protected collection: PaginatorCollection,
63+
protected dataProp: DataProp,
64+
protected metaData: MetaData
65+
) {}
66+
67+
setDataProp<Value extends string>(
68+
dataProp: Value
69+
): Paginator<PaginatorCollection, Value, MetaData> {
70+
return new Paginator(this.collection, dataProp, this.metaData)
71+
}
72+
73+
setMetaData<Value extends Record<string, any>>(
74+
metaData: Value | ((data: MetaData) => Value)
75+
): Paginator<PaginatorCollection, DataProp, Value> {
76+
return new Paginator(
77+
this.collection,
78+
this.dataProp,
79+
typeof metaData === 'function' ? metaData(this.metaData) : metaData
80+
)
81+
}
82+
83+
/**
84+
* Serializes the paginated data by combining the serialized collection
85+
* with pagination metadata
86+
*
87+
* @param container - Container resolver for dependency injection
88+
* @param depth - Current depth level in the transformation tree
89+
* @param maxDepth - Optional maximum depth override
90+
*
91+
* @example
92+
* ```ts
93+
* const paginator = new Paginator(collection, 'users', { page: 1, total: 50 })
94+
* const result = await paginator.serialize(container, 0, 2)
95+
* // Result: { users: [...serialized items...], page: 1, total: 50 }
96+
* ```
97+
*/
98+
async serialize(
99+
container: ContainerResolver<any>,
100+
depth: number,
101+
maxDepth?: number
102+
): Promise<
103+
{
104+
[M in DataProp]: UnpackAsCollection<PaginatorCollection, -1, 0, true>
105+
} & MetaData
106+
> {
107+
return {
108+
[this.dataProp]: await this.collection.serialize(container, depth, maxDepth),
109+
...this.metaData,
110+
}
111+
}
112+
}

0 commit comments

Comments
 (0)