Skip to content

Commit

Permalink
feat: introduce per-serializer context
Browse files Browse the repository at this point in the history
BREAKING CHANGE: the serializer context is now not global anymore put per
serializer.
  • Loading branch information
DASPRiD committed Apr 21, 2024
1 parent 1000b83 commit b133213
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 50 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ type SerializerContext = {
};
```

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

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

You can then serialize your entities in the following way:

```typescript
serializeManager.createResourceDocument("my_entity", entity, {
context: {my_entity: {permissions: "foo"}},
});
```

#### Filtering

You can add filters to list handlers as well. JSON:API does not define the structure of filters itself, except that the
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export {
type EntitySerializer,
type SerializerOptions,
type SerializeManagerOptions,
type InferManagerContext,
SerializeManager,
} from "./serializer.js";
36 changes: 16 additions & 20 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,22 +357,21 @@ const listQuerySchema = baseQuerySchema.extend({
.optional(),
});

type ParseBaseQueryOptions<TContext> = {
context?: TContext;
type ParseBaseQueryOptions = {
defaultFields?: Record<string, string[]>;
defaultInclude?: string[];
};

type ParseBaseQueryResult<TContext> = {
serializerOptions: SerializeManagerOptions<TContext>;
type ParseBaseQueryResult = {
// biome-ignore lint/suspicious/noExplicitAny: required for inference
serializerOptions: SerializeManagerOptions<any>;
};

type ParseListQueryOptions<
TContext,
TSort extends string,
TFilterSchema extends z.ZodType<unknown> | undefined,
TPageSchema extends z.ZodType<unknown> | undefined,
> = ParseBaseQueryOptions<TContext> & {
> = ParseBaseQueryOptions & {
defaultSort?: Sort<TSort>;
allowedSortFields?: TSort[];
filterSchema?: TFilterSchema;
Expand All @@ -384,11 +383,10 @@ type OptionalSchema<T extends z.ZodType<unknown> | undefined> = T extends z.ZodT
: z.ZodUndefined;

type ParseListQueryResult<
TContext,
TSort extends string,
TFilterSchema extends z.ZodType<unknown> | undefined,
TPageSchema extends z.ZodType<unknown> | undefined,
> = ParseBaseQueryResult<TContext> & {
> = ParseBaseQueryResult & {
sort?: Sort<TSort>;
filter: z.output<OptionalSchema<TFilterSchema>>;
page: z.output<OptionalSchema<TPageSchema>>;
Expand Down Expand Up @@ -437,25 +435,24 @@ export const parseQuerySchema = <T extends z.ZodType<unknown>>(
return parseResult.data;
};

const createSerializerOptions = <TContext>(
const createSerializerOptions = (
options:
| ParseBaseQueryOptions<TContext>
| ParseListQueryOptions<TContext, string, undefined, undefined>
| ParseBaseQueryOptions
| ParseListQueryOptions<string, undefined, undefined>
| undefined,
result: z.output<typeof baseQuerySchema>,
): SerializeManagerOptions<TContext> => ({
context: options?.context,
): SerializeManagerOptions => ({
fields: {
...options?.defaultFields,
...result.fields,
},
include: result.include ?? options?.defaultInclude,
});

export const parseBaseQuery = <TContext = undefined>(
export const parseBaseQuery = (
koaContext: Context,
options: ParseBaseQueryOptions<TContext>,
): ParseBaseQueryResult<TContext> => {
options: ParseBaseQueryOptions,
): ParseBaseQueryResult => {
const result = parseQuerySchema(koaContext, baseQuerySchema);

return {
Expand All @@ -477,7 +474,7 @@ const getListQuerySchema = <
TFilterSchema extends z.ZodType<unknown> | undefined = undefined,
TPageSchema extends z.ZodType<unknown> | undefined = undefined,
>(
options?: ParseListQueryOptions<unknown, string, TFilterSchema, TPageSchema>,
options?: ParseListQueryOptions<string, TFilterSchema, TPageSchema>,
): MergedListQuerySchema<TFilterSchema, TPageSchema> => {
return listQuerySchema.extend({
filter: (options?.filterSchema ?? z.undefined()) as OptionalSchema<TFilterSchema>,
Expand All @@ -486,14 +483,13 @@ const getListQuerySchema = <
};

export const parseListQuery = <
TContext = undefined,
TSort extends string = string,
TFilterSchema extends z.ZodType<unknown> | undefined = undefined,
TPageSchema extends z.ZodType<unknown> | undefined = undefined,
>(
koaContext: Context,
options?: ParseListQueryOptions<TContext, TSort, TFilterSchema, TPageSchema>,
): ParseListQueryResult<TContext, TSort, TFilterSchema, TPageSchema> => {
options?: ParseListQueryOptions<TSort, TFilterSchema, TPageSchema>,
): ParseListQueryResult<TSort, TFilterSchema, TPageSchema> => {
const result = parseQuerySchema(koaContext, getListQuerySchema(options));

return {
Expand Down
66 changes: 39 additions & 27 deletions src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export type EntitySerializer<TEntity, TReference, TContext = undefined, TSideloa
};

// biome-ignore lint/suspicious/noExplicitAny: required for inference
type AnySerializeManager = SerializeManager<any, any>;
type AnySerializeManager = SerializeManager<any>;
// biome-ignore lint/suspicious/noExplicitAny: required for inference
type Serializers<TContext> = Record<string, EntitySerializer<any, any, TContext, any>>;
type Serializers = Record<string, EntitySerializer<any, any, any, any>>;
// biome-ignore lint/suspicious/noExplicitAny: required for inference
type InferEntity<TSerializer> = TSerializer extends EntitySerializer<infer T, any, any, any>
? T
Expand All @@ -84,18 +84,29 @@ type InferReference<TSerializer> = TSerializer extends EntitySerializer<any, inf
? T
: never;
// biome-ignore lint/suspicious/noExplicitAny: required for inference
type InferSideloaded<TSerializer> = TSerializer extends EntitySerializer<any, any, any, infer T>
type InferContext<TSerializer> = TSerializer extends EntitySerializer<any, any, infer T, any>
? T | undefined
: never;
// biome-ignore lint/suspicious/noExplicitAny: required for inference
type InferKeys<TSerializers extends Serializers<any>> = keyof TSerializers & string;
// biome-ignore lint/suspicious/noExplicitAny: required for inference
type InferSerializers<TSerializeManager> = TSerializeManager extends SerializeManager<any, infer T>
type InferSideloaded<TSerializer> = TSerializer extends EntitySerializer<any, any, any, infer T>
? T | undefined
: never;
type InferKeys<TSerializers extends Serializers> = keyof TSerializers & string;
type InferSerializers<TSerializeManager> = TSerializeManager extends SerializeManager<infer T>
? T
: never;
type ManagerContext<TSerializers extends Serializers> = {
[K in keyof TSerializers]?: InferContext<TSerializers[K]>;
};
export type InferManagerContext<TSerializeManager extends AnySerializeManager> = ManagerContext<
InferSerializers<TSerializeManager>
>;

export type SerializeManagerOptions<TContext = undefined, TSideloaded = undefined> = {
context?: TContext;
export type SerializeManagerOptions<
TSerializers extends Serializers = Serializers,
TSideloaded = undefined,
> = {
context?: ManagerContext<TSerializers>;
fields?: Record<string, string[]>;
include?: string[];
meta?: Meta;
Expand All @@ -110,21 +121,18 @@ type SerializeEntityResult = {
entityRelationships?: EntityRelationships;
};

export class SerializeManager<
TContext = undefined,
TSerializers extends Serializers<TContext> = Serializers<TContext>,
> {
export class SerializeManager<TSerializers extends Serializers = Serializers> {
public constructor(private readonly serializers: TSerializers) {
this.serializers = serializers;
}

public createResourceDocument<TType extends InferKeys<TSerializers>>(
type: TType,
entity: InferEntity<TSerializers[TType]>,
options?: SerializeManagerOptions<TContext, InferSideloaded<TSerializers[TType]>>,
options?: SerializeManagerOptions<TSerializers, InferSideloaded<TSerializers[TType]>>,
): JsonApiBody {
const serializeEntityResult = this.serializeEntity(type, entity, options);
let included: IncludedCollection<TContext, typeof this> | undefined = undefined;
let included: IncludedCollection<TSerializers, typeof this> | undefined = undefined;

if (options?.include && serializeEntityResult.entityRelationships) {
included = new IncludedCollection(this);
Expand All @@ -140,12 +148,12 @@ export class SerializeManager<
public createMultiResourceDocument<TType extends InferKeys<TSerializers>>(
type: TType,
entities: InferEntity<TSerializers[TType]>[],
options?: SerializeManagerOptions<TContext, InferSideloaded<TSerializers[TType]>>,
options?: SerializeManagerOptions<TSerializers, InferSideloaded<TSerializers[TType]>>,
): JsonApiBody {
const serializeEntityResults = entities.map((entity) =>
this.serializeEntity(type, entity, options),
);
let included: IncludedCollection<TContext, typeof this> | undefined = undefined;
let included: IncludedCollection<TSerializers, typeof this> | undefined = undefined;

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

private createJsonApiBody(
data: Resource | Resource[] | null,
options?: SerializeManagerOptions<TContext, unknown>,
included?: IncludedCollection<TContext, this>,
options?: SerializeManagerOptions<TSerializers, unknown>,
included?: IncludedCollection<TSerializers, this>,
): JsonApiBody {
return new JsonApiBody(
{
Expand Down Expand Up @@ -205,13 +213,13 @@ export class SerializeManager<
public serializeEntity<TType extends InferKeys<TSerializers>>(
type: TType,
entity: InferEntity<TSerializers[TType]>,
options?: SerializeManagerOptions<TContext, InferSideloaded<TSerializers[TType]>>,
options?: SerializeManagerOptions<TSerializers, InferSideloaded<TSerializers[TType]>>,
): SerializeEntityResult {
const serializerOptions: SerializerOptions<
TContext,
TSerializers,
InferSideloaded<TSerializers[TType]>
> = {
context: options?.context,
context: options?.context?.[type],
sideloaded: options?.sideloaded,
};

Expand Down Expand Up @@ -307,19 +315,23 @@ export class SerializeManager<
}
}

type AddToIncludedCollectionOptions<TContext> = SerializeManagerOptions<TContext> & {
include: string[];
};
type AddToIncludedCollectionOptions<TSerializers extends Serializers> =
SerializeManagerOptions<TSerializers> & {
include: string[];
};

// biome-ignore lint/suspicious/noExplicitAny: required for inference

Check warning on line 323 in src/serializer.ts

View workflow job for this annotation

GitHub Actions / Release

Suppression comment is not being used
class IncludedCollection<TContext, TSerializeManager extends SerializeManager<TContext, any>> {
class IncludedCollection<
TSerializers extends Serializers,
TSerializeManager extends SerializeManager<any>,

Check failure on line 326 in src/serializer.ts

View workflow job for this annotation

GitHub Actions / Release

Unexpected any. Specify a different type.
> {
private included = new Map<string, Resource>();

public constructor(private readonly serializeManager: TSerializeManager) {}

public add(
entityRelationships: EntityRelationships<TSerializeManager>,
options: AddToIncludedCollectionOptions<TContext>,
options: AddToIncludedCollectionOptions<TSerializers>,
parentFieldPath = "",
): void {
for (const [field, entityRelationship] of Object.entries(entityRelationships)) {
Expand Down Expand Up @@ -360,7 +372,7 @@ class IncludedCollection<TContext, TSerializeManager extends SerializeManager<TC
private addSingle(
entityRelationship: EntityRelationship<TSerializeManager>,
field: string,
options: AddToIncludedCollectionOptions<TContext>,
options: AddToIncludedCollectionOptions<TSerializers>,
): void {
const id = this.serializeManager.getEntityRelationshipId(entityRelationship);
const compositeKey = `${entityRelationship.type}:${id}`;
Expand Down

0 comments on commit b133213

Please sign in to comment.