Skip to content

Commit b133213

Browse files
committed
feat: introduce per-serializer context
BREAKING CHANGE: the serializer context is now not global anymore put per serializer.
1 parent 1000b83 commit b133213

File tree

4 files changed

+66
-50
lines changed

4 files changed

+66
-50
lines changed

README.md

+10-3
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ type SerializerContext = {
5252
};
5353
```
5454

55-
You can the define that context as generic parameter in both the `SerializeManager` as well as your `EntitySerializer`
56-
instances. Since the context is always an optional property, you can then perform checks like this in e.g. your
57-
`getAttributes()` implementation:
55+
Each serializer can define its own context, and it will be accessible separately for each serializer. Since the context
56+
is always an optional property, you can then perform checks like this in e.g. your `getAttributes()` implementation:
5857

5958
```typescript
6059
const attributes: Record<string, unknown> = {
@@ -67,6 +66,14 @@ if (options.context?.permissions?.includes("read:secret")) {
6766
}
6867
```
6968

69+
You can then serialize your entities in the following way:
70+
71+
```typescript
72+
serializeManager.createResourceDocument("my_entity", entity, {
73+
context: {my_entity: {permissions: "foo"}},
74+
});
75+
```
76+
7077
#### Filtering
7178

7279
You can add filters to list handlers as well. JSON:API does not define the structure of filters itself, except that the

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ export {
2525
type EntitySerializer,
2626
type SerializerOptions,
2727
type SerializeManagerOptions,
28+
type InferManagerContext,
2829
SerializeManager,
2930
} from "./serializer.js";

src/request.ts

+16-20
Original file line numberDiff line numberDiff line change
@@ -357,22 +357,21 @@ const listQuerySchema = baseQuerySchema.extend({
357357
.optional(),
358358
});
359359

360-
type ParseBaseQueryOptions<TContext> = {
361-
context?: TContext;
360+
type ParseBaseQueryOptions = {
362361
defaultFields?: Record<string, string[]>;
363362
defaultInclude?: string[];
364363
};
365364

366-
type ParseBaseQueryResult<TContext> = {
367-
serializerOptions: SerializeManagerOptions<TContext>;
365+
type ParseBaseQueryResult = {
366+
// biome-ignore lint/suspicious/noExplicitAny: required for inference
367+
serializerOptions: SerializeManagerOptions<any>;
368368
};
369369

370370
type ParseListQueryOptions<
371-
TContext,
372371
TSort extends string,
373372
TFilterSchema extends z.ZodType<unknown> | undefined,
374373
TPageSchema extends z.ZodType<unknown> | undefined,
375-
> = ParseBaseQueryOptions<TContext> & {
374+
> = ParseBaseQueryOptions & {
376375
defaultSort?: Sort<TSort>;
377376
allowedSortFields?: TSort[];
378377
filterSchema?: TFilterSchema;
@@ -384,11 +383,10 @@ type OptionalSchema<T extends z.ZodType<unknown> | undefined> = T extends z.ZodT
384383
: z.ZodUndefined;
385384

386385
type ParseListQueryResult<
387-
TContext,
388386
TSort extends string,
389387
TFilterSchema extends z.ZodType<unknown> | undefined,
390388
TPageSchema extends z.ZodType<unknown> | undefined,
391-
> = ParseBaseQueryResult<TContext> & {
389+
> = ParseBaseQueryResult & {
392390
sort?: Sort<TSort>;
393391
filter: z.output<OptionalSchema<TFilterSchema>>;
394392
page: z.output<OptionalSchema<TPageSchema>>;
@@ -437,25 +435,24 @@ export const parseQuerySchema = <T extends z.ZodType<unknown>>(
437435
return parseResult.data;
438436
};
439437

440-
const createSerializerOptions = <TContext>(
438+
const createSerializerOptions = (
441439
options:
442-
| ParseBaseQueryOptions<TContext>
443-
| ParseListQueryOptions<TContext, string, undefined, undefined>
440+
| ParseBaseQueryOptions
441+
| ParseListQueryOptions<string, undefined, undefined>
444442
| undefined,
445443
result: z.output<typeof baseQuerySchema>,
446-
): SerializeManagerOptions<TContext> => ({
447-
context: options?.context,
444+
): SerializeManagerOptions => ({
448445
fields: {
449446
...options?.defaultFields,
450447
...result.fields,
451448
},
452449
include: result.include ?? options?.defaultInclude,
453450
});
454451

455-
export const parseBaseQuery = <TContext = undefined>(
452+
export const parseBaseQuery = (
456453
koaContext: Context,
457-
options: ParseBaseQueryOptions<TContext>,
458-
): ParseBaseQueryResult<TContext> => {
454+
options: ParseBaseQueryOptions,
455+
): ParseBaseQueryResult => {
459456
const result = parseQuerySchema(koaContext, baseQuerySchema);
460457

461458
return {
@@ -477,7 +474,7 @@ const getListQuerySchema = <
477474
TFilterSchema extends z.ZodType<unknown> | undefined = undefined,
478475
TPageSchema extends z.ZodType<unknown> | undefined = undefined,
479476
>(
480-
options?: ParseListQueryOptions<unknown, string, TFilterSchema, TPageSchema>,
477+
options?: ParseListQueryOptions<string, TFilterSchema, TPageSchema>,
481478
): MergedListQuerySchema<TFilterSchema, TPageSchema> => {
482479
return listQuerySchema.extend({
483480
filter: (options?.filterSchema ?? z.undefined()) as OptionalSchema<TFilterSchema>,
@@ -486,14 +483,13 @@ const getListQuerySchema = <
486483
};
487484

488485
export const parseListQuery = <
489-
TContext = undefined,
490486
TSort extends string = string,
491487
TFilterSchema extends z.ZodType<unknown> | undefined = undefined,
492488
TPageSchema extends z.ZodType<unknown> | undefined = undefined,
493489
>(
494490
koaContext: Context,
495-
options?: ParseListQueryOptions<TContext, TSort, TFilterSchema, TPageSchema>,
496-
): ParseListQueryResult<TContext, TSort, TFilterSchema, TPageSchema> => {
491+
options?: ParseListQueryOptions<TSort, TFilterSchema, TPageSchema>,
492+
): ParseListQueryResult<TSort, TFilterSchema, TPageSchema> => {
497493
const result = parseQuerySchema(koaContext, getListQuerySchema(options));
498494

499495
return {

src/serializer.ts

+39-27
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ export type EntitySerializer<TEntity, TReference, TContext = undefined, TSideloa
7272
};
7373

7474
// biome-ignore lint/suspicious/noExplicitAny: required for inference
75-
type AnySerializeManager = SerializeManager<any, any>;
75+
type AnySerializeManager = SerializeManager<any>;
7676
// biome-ignore lint/suspicious/noExplicitAny: required for inference
77-
type Serializers<TContext> = Record<string, EntitySerializer<any, any, TContext, any>>;
77+
type Serializers = Record<string, EntitySerializer<any, any, any, any>>;
7878
// biome-ignore lint/suspicious/noExplicitAny: required for inference
7979
type InferEntity<TSerializer> = TSerializer extends EntitySerializer<infer T, any, any, any>
8080
? T
@@ -84,18 +84,29 @@ type InferReference<TSerializer> = TSerializer extends EntitySerializer<any, inf
8484
? T
8585
: never;
8686
// biome-ignore lint/suspicious/noExplicitAny: required for inference
87-
type InferSideloaded<TSerializer> = TSerializer extends EntitySerializer<any, any, any, infer T>
87+
type InferContext<TSerializer> = TSerializer extends EntitySerializer<any, any, infer T, any>
8888
? T | undefined
8989
: never;
9090
// biome-ignore lint/suspicious/noExplicitAny: required for inference
91-
type InferKeys<TSerializers extends Serializers<any>> = keyof TSerializers & string;
92-
// biome-ignore lint/suspicious/noExplicitAny: required for inference
93-
type InferSerializers<TSerializeManager> = TSerializeManager extends SerializeManager<any, infer T>
91+
type InferSideloaded<TSerializer> = TSerializer extends EntitySerializer<any, any, any, infer T>
92+
? T | undefined
93+
: never;
94+
type InferKeys<TSerializers extends Serializers> = keyof TSerializers & string;
95+
type InferSerializers<TSerializeManager> = TSerializeManager extends SerializeManager<infer T>
9496
? T
9597
: never;
98+
type ManagerContext<TSerializers extends Serializers> = {
99+
[K in keyof TSerializers]?: InferContext<TSerializers[K]>;
100+
};
101+
export type InferManagerContext<TSerializeManager extends AnySerializeManager> = ManagerContext<
102+
InferSerializers<TSerializeManager>
103+
>;
96104

97-
export type SerializeManagerOptions<TContext = undefined, TSideloaded = undefined> = {
98-
context?: TContext;
105+
export type SerializeManagerOptions<
106+
TSerializers extends Serializers = Serializers,
107+
TSideloaded = undefined,
108+
> = {
109+
context?: ManagerContext<TSerializers>;
99110
fields?: Record<string, string[]>;
100111
include?: string[];
101112
meta?: Meta;
@@ -110,21 +121,18 @@ type SerializeEntityResult = {
110121
entityRelationships?: EntityRelationships;
111122
};
112123

113-
export class SerializeManager<
114-
TContext = undefined,
115-
TSerializers extends Serializers<TContext> = Serializers<TContext>,
116-
> {
124+
export class SerializeManager<TSerializers extends Serializers = Serializers> {
117125
public constructor(private readonly serializers: TSerializers) {
118126
this.serializers = serializers;
119127
}
120128

121129
public createResourceDocument<TType extends InferKeys<TSerializers>>(
122130
type: TType,
123131
entity: InferEntity<TSerializers[TType]>,
124-
options?: SerializeManagerOptions<TContext, InferSideloaded<TSerializers[TType]>>,
132+
options?: SerializeManagerOptions<TSerializers, InferSideloaded<TSerializers[TType]>>,
125133
): JsonApiBody {
126134
const serializeEntityResult = this.serializeEntity(type, entity, options);
127-
let included: IncludedCollection<TContext, typeof this> | undefined = undefined;
135+
let included: IncludedCollection<TSerializers, typeof this> | undefined = undefined;
128136

129137
if (options?.include && serializeEntityResult.entityRelationships) {
130138
included = new IncludedCollection(this);
@@ -140,12 +148,12 @@ export class SerializeManager<
140148
public createMultiResourceDocument<TType extends InferKeys<TSerializers>>(
141149
type: TType,
142150
entities: InferEntity<TSerializers[TType]>[],
143-
options?: SerializeManagerOptions<TContext, InferSideloaded<TSerializers[TType]>>,
151+
options?: SerializeManagerOptions<TSerializers, InferSideloaded<TSerializers[TType]>>,
144152
): JsonApiBody {
145153
const serializeEntityResults = entities.map((entity) =>
146154
this.serializeEntity(type, entity, options),
147155
);
148-
let included: IncludedCollection<TContext, typeof this> | undefined = undefined;
156+
let included: IncludedCollection<TSerializers, typeof this> | undefined = undefined;
149157

150158
if (options?.include) {
151159
included = new IncludedCollection(this);
@@ -171,8 +179,8 @@ export class SerializeManager<
171179

172180
private createJsonApiBody(
173181
data: Resource | Resource[] | null,
174-
options?: SerializeManagerOptions<TContext, unknown>,
175-
included?: IncludedCollection<TContext, this>,
182+
options?: SerializeManagerOptions<TSerializers, unknown>,
183+
included?: IncludedCollection<TSerializers, this>,
176184
): JsonApiBody {
177185
return new JsonApiBody(
178186
{
@@ -205,13 +213,13 @@ export class SerializeManager<
205213
public serializeEntity<TType extends InferKeys<TSerializers>>(
206214
type: TType,
207215
entity: InferEntity<TSerializers[TType]>,
208-
options?: SerializeManagerOptions<TContext, InferSideloaded<TSerializers[TType]>>,
216+
options?: SerializeManagerOptions<TSerializers, InferSideloaded<TSerializers[TType]>>,
209217
): SerializeEntityResult {
210218
const serializerOptions: SerializerOptions<
211-
TContext,
219+
TSerializers,
212220
InferSideloaded<TSerializers[TType]>
213221
> = {
214-
context: options?.context,
222+
context: options?.context?.[type],
215223
sideloaded: options?.sideloaded,
216224
};
217225

@@ -307,19 +315,23 @@ export class SerializeManager<
307315
}
308316
}
309317

310-
type AddToIncludedCollectionOptions<TContext> = SerializeManagerOptions<TContext> & {
311-
include: string[];
312-
};
318+
type AddToIncludedCollectionOptions<TSerializers extends Serializers> =
319+
SerializeManagerOptions<TSerializers> & {
320+
include: string[];
321+
};
313322

314323
// biome-ignore lint/suspicious/noExplicitAny: required for inference
315-
class IncludedCollection<TContext, TSerializeManager extends SerializeManager<TContext, any>> {
324+
class IncludedCollection<
325+
TSerializers extends Serializers,
326+
TSerializeManager extends SerializeManager<any>,
327+
> {
316328
private included = new Map<string, Resource>();
317329

318330
public constructor(private readonly serializeManager: TSerializeManager) {}
319331

320332
public add(
321333
entityRelationships: EntityRelationships<TSerializeManager>,
322-
options: AddToIncludedCollectionOptions<TContext>,
334+
options: AddToIncludedCollectionOptions<TSerializers>,
323335
parentFieldPath = "",
324336
): void {
325337
for (const [field, entityRelationship] of Object.entries(entityRelationships)) {
@@ -360,7 +372,7 @@ class IncludedCollection<TContext, TSerializeManager extends SerializeManager<TC
360372
private addSingle(
361373
entityRelationship: EntityRelationship<TSerializeManager>,
362374
field: string,
363-
options: AddToIncludedCollectionOptions<TContext>,
375+
options: AddToIncludedCollectionOptions<TSerializers>,
364376
): void {
365377
const id = this.serializeManager.getEntityRelationshipId(entityRelationship);
366378
const compositeKey = `${entityRelationship.type}:${id}`;

0 commit comments

Comments
 (0)