Skip to content

Commit ca682f7

Browse files
committed
Full dynamic typing of ItemInstancesHrefSchema
1 parent f11b6b3 commit ca682f7

11 files changed

Lines changed: 523 additions & 33 deletions

File tree

generate/applyGenericsToSchema.ts

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ import ts from 'typescript';
2222
* - Special rule: if the top-level property is "included",
2323
* bare Item references inside it remain plain `Item` (no generic).
2424
*
25-
* 4. Item schemas
25+
* 4. Item href schemas
26+
* (e.g. ItemInstancesHrefSchema)
27+
* - Add generic:
28+
* <D extends ItemTypeDefinition = ItemTypeDefinition>
29+
* - Replace fields?: { [k: string]: unknown } with:
30+
* fields?: ToItemHrefSchemaField<D>
31+
* - Replace order_by?: string with:
32+
* order_by?: ToItemHrefSchemaOrderBy<D>
33+
*
34+
* 5. Item schemas
2635
* (ItemValidateExistingSchema, ItemValidateNewSchema, ItemCreateSchema, ItemUpdateSchema)
2736
* - Add generic:
2837
* <D extends ItemTypeDefinition = ItemTypeDefinition>
@@ -36,7 +45,7 @@ import ts from 'typescript';
3645
* - If type has a "data" property, add it inside the "data" object
3746
* - Otherwise, add it at the top level
3847
*
39-
* 5. ItemRelationships
48+
* 6. ItemRelationships
4049
* - Add generic:
4150
* <D extends ItemTypeDefinition = ItemTypeDefinition>
4251
* - Replace ItemTypeData with ItemTypeData<D>
@@ -87,7 +96,10 @@ export function applyGenerics(sourceCode: string): string {
8796
'ScheduledUnpublishingDestroyTargetSchema',
8897
]);
8998

90-
// 4. Item schemas
99+
// 4. Item href schemas
100+
const itemHrefSchemas = new Set(['ItemInstancesHrefSchema']);
101+
102+
// 5. Item schemas
91103
const itemSchemas = new Set([
92104
'ItemValidateExistingSchema',
93105
'ItemValidateNewSchema',
@@ -196,6 +208,65 @@ export function applyGenerics(sourceCode: string): string {
196208
);
197209
}
198210

211+
function transformItemHrefSchema(
212+
node: ts.TypeAliasDeclaration,
213+
context: ts.TransformationContext,
214+
): ts.TypeAliasDeclaration {
215+
const visit: ts.Visitor = (child: ts.Node): ts.Node | undefined => {
216+
// Replace fields?: { [k: string]: unknown } with fields?: ToItemHrefSchemaField<D>
217+
if (
218+
ts.isPropertySignature(child) &&
219+
ts.isIdentifier(child.name) &&
220+
child.name.text === 'fields' &&
221+
child.type &&
222+
ts.isTypeLiteralNode(child.type) &&
223+
child.type.members.length === 1 &&
224+
ts.isIndexSignatureDeclaration(child.type.members[0]!)
225+
) {
226+
return ts.factory.updatePropertySignature(
227+
child,
228+
child.modifiers,
229+
child.name,
230+
child.questionToken,
231+
ts.factory.createTypeReferenceNode('ToItemHrefSchemaField', [
232+
ts.factory.createTypeReferenceNode('D', undefined),
233+
]),
234+
);
235+
}
236+
237+
// Replace order_by?: string with order_by?: ToItemHrefSchemaOrderBy<D>
238+
if (
239+
ts.isPropertySignature(child) &&
240+
ts.isIdentifier(child.name) &&
241+
child.name.text === 'order_by' &&
242+
child.type &&
243+
child.type.kind === ts.SyntaxKind.StringKeyword
244+
) {
245+
return ts.factory.updatePropertySignature(
246+
child,
247+
child.modifiers,
248+
child.name,
249+
child.questionToken,
250+
ts.factory.createTypeReferenceNode('ToItemHrefSchemaOrderBy', [
251+
ts.factory.createTypeReferenceNode('D', undefined),
252+
]),
253+
);
254+
}
255+
256+
return ts.visitEachChild(child, visit, context);
257+
};
258+
259+
const transformedType = ts.visitNode(node.type, visit) as ts.TypeNode;
260+
261+
return ts.factory.updateTypeAliasDeclaration(
262+
node,
263+
node.modifiers,
264+
ts.factory.createIdentifier(node.name.text),
265+
[genericParamD],
266+
transformedType,
267+
);
268+
}
269+
199270
function transformItemRelationships(
200271
node: ts.TypeAliasDeclaration,
201272
context: ts.TransformationContext,
@@ -405,6 +476,10 @@ export function applyGenerics(sourceCode: string): string {
405476
return transformItemTargetSchema(node, context);
406477
}
407478

479+
if (itemHrefSchemas.has(name) && ts.isTypeAliasDeclaration(node)) {
480+
return transformItemHrefSchema(node, context);
481+
}
482+
408483
if (itemSchemas.has(name) && ts.isTypeAliasDeclaration(node)) {
409484
return transformItemSchema(node, context);
410485
}

generate/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ async function generate(prefix: string, hyperschemaUrl: string) {
118118
{
119119
...resourceInfo,
120120
isCma,
121+
endpoints: resourceInfo.endpoints.map((endpoint) => ({
122+
...endpoint,
123+
isItemInstances:
124+
resourceInfo.jsonApiType === 'item' && endpoint.rel === 'instances',
125+
})),
121126
someEndpointReturnsItem: resourceInfo.endpoints.some(
122127
(e) => e.returnsItem,
123128
),

generate/templates/ApiTypes.ts.handlebars

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
ToItemAttributes,
1111
ToItemAttributesInRequest,
1212
ToItemAttributesInNestedResponse,
13+
ToItemHrefSchemaField,
14+
ToItemHrefSchemaOrderBy,
1315
} from '../utilities/itemDefinition';
1416

1517
{{#if simpleVariant}}

generate/templates/resources/ResourceClass.ts.handlebars

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
3636
body{{#if optionalRequestBody}}?{{/if}}: ApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
3737
{{/if}}
3838
{{#if queryParamsType}}
39-
queryParams: ApiTypes.{{{queryParamsType}}} & { nested: true },
39+
queryParams: ApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}} & { nested: true },
4040
{{/if}}
4141
): Promise<
4242
{{#if responseType}}
@@ -53,7 +53,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
5353
body{{#if optionalRequestBody}}?{{/if}}: ApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
5454
{{/if}}
5555
{{#if queryParamsType}}
56-
queryParams?: ApiTypes.{{{queryParamsType}}} & { nested?: false | undefined },
56+
queryParams?: ApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}} & { nested?: false | undefined },
5757
{{/if}}
5858
): Promise<
5959
{{#if responseType}}
@@ -101,7 +101,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
101101
body{{#if optionalRequestBody}}?{{/if}}: ApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
102102
{{/if}}
103103
{{#if queryParamsType}}
104-
queryParams{{#unless queryParamsRequired}}?{{/unless}}: ApiTypes.{{{queryParamsType}}},
104+
queryParams{{#unless queryParamsRequired}}?{{/unless}}: ApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}},
105105
{{/if}}
106106
) {
107107
return this.{{{rawName}}}{{#if returnsItem}}<D>{{/if}}(
@@ -158,7 +158,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
158158
body{{#if optionalRequestBody}}?{{/if}}: RawApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
159159
{{/if}}
160160
{{#if queryParamsType}}
161-
queryParams: RawApiTypes.{{{queryParamsType}}} & { nested: true },
161+
queryParams: RawApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}} & { nested: true },
162162
{{/if}}
163163
): Promise<
164164
{{#if responseType}}
@@ -175,7 +175,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
175175
body{{#if optionalRequestBody}}?{{/if}}: RawApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
176176
{{/if}}
177177
{{#if queryParamsType}}
178-
queryParams?: RawApiTypes.{{{queryParamsType}}} & { nested?: false | undefined },
178+
queryParams?: RawApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}} & { nested?: false | undefined },
179179
{{/if}}
180180
): Promise<
181181
{{#if responseType}}
@@ -208,7 +208,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
208208
{{#if requestBodyType}}
209209
body{{#if optionalRequestBody}}?{{/if}}: RawApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
210210
{{/if}}
211-
queryParams{{#unless queryParamsRequired}}?{{/unless}}: RawApiTypes.{{{queryParamsType}}},
211+
queryParams{{#unless queryParamsRequired}}?{{/unless}}: RawApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}},
212212
): Promise<
213213
{{#if responseType}}
214214
RawApiTypes.{{{responseType}}}{{#if returnsItem}}<NoInfer<D>, true>{{/if}}
@@ -240,7 +240,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
240240
body{{#if optionalRequestBody}}?{{/if}}: RawApiTypes.{{{requestBodyType}}}{{#if requestBodyRequiresItem}}<NoInfer<D>>{{/if}},
241241
{{/if}}
242242
{{#if queryParamsType}}
243-
queryParams{{#unless queryParamsRequired}}?{{/unless}}: RawApiTypes.{{{queryParamsType}}},
243+
queryParams{{#unless queryParamsRequired}}?{{/unless}}: RawApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}},
244244
{{/if}}
245245
){{#unless offersNestedItemsOptionInQueryParams}}: Promise<
246246
{{#if responseType}}
@@ -284,7 +284,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
284284
{{#each urlPlaceholders}}
285285
{{{variableName}}}: string | ApiTypes.{{{relType}}},
286286
{{/each}}
287-
queryParams{{#unless queryParamsRequired}}?{{/unless}}: Utils.OmitFromKnownKeys<ApiTypes.{{{queryParamsType}}}, 'page'>,
287+
queryParams{{#unless queryParamsRequired}}?{{/unless}}: Utils.OmitFromKnownKeys<ApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}}, 'page'>,
288288
iteratorOptions?: Utils.IteratorOptions,
289289
) {
290290
for await (const element of this.{{{rawName}}}PagedIterator{{#if returnsItem}}<NoInfer<D>>{{/if}}(
@@ -315,7 +315,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
315315
{{#each urlPlaceholders}}
316316
{{{variableName}}}: string,
317317
{{/each}}
318-
queryParams{{#unless queryParamsRequired}}?{{/unless}}: Utils.OmitFromKnownKeys<RawApiTypes.{{{queryParamsType}}}, 'page'>,
318+
queryParams{{#unless queryParamsRequired}}?{{/unless}}: Utils.OmitFromKnownKeys<RawApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}}, 'page'>,
319319
iteratorOptions?: Utils.IteratorOptions,
320320
) {
321321
Utils.warnOnPageQueryParam(queryParams);
@@ -327,7 +327,7 @@ export default class {{{resourceClassName}}} extends BaseResource {
327327
defaultLimit: {{{paginatedResponse.defaultLimit}}},
328328
maxLimit: {{{paginatedResponse.maxLimit}}},
329329
},
330-
(page: RawApiTypes.{{{queryParamsType}}}['page']) => this.{{{rawName}}}{{#if returnsItem}}<D>{{/if}}(
330+
(page: RawApiTypes.{{{queryParamsType}}}{{#if isItemInstances}}<D>{{/if}}['page']) => this.{{{rawName}}}{{#if returnsItem}}<D>{{/if}}(
331331
{{#each urlPlaceholders}}
332332
{{{variableName}}},
333333
{{/each}}

packages/cma-client-browser/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export type {
244244
ToItemAttributes,
245245
ToItemAttributesInNestedResponse,
246246
ToItemAttributesInRequest,
247+
ToItemHrefSchemaField,
248+
ToItemHrefSchemaOrderBy,
247249
TraversalDirection,
248250
TreePath,
249251
UnchangedBlockInRequest,

packages/cma-client-node/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ export type {
244244
ToItemAttributes,
245245
ToItemAttributesInNestedResponse,
246246
ToItemAttributesInRequest,
247+
ToItemHrefSchemaField,
248+
ToItemHrefSchemaOrderBy,
247249
TraversalDirection,
248250
TreePath,
249251
UnchangedBlockInRequest,

packages/cma-client/src/generated/ApiTypes.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
ToItemAttributes,
1010
ToItemAttributesInNestedResponse,
1111
ToItemAttributesInRequest,
12+
ToItemHrefSchemaField,
13+
ToItemHrefSchemaOrderBy,
1214
} from '../utilities/itemDefinition';
1315

1416
export type Field = GenericFieldAttributes<FieldStableShell>;
@@ -853,7 +855,9 @@ export type ItemInstancesTargetSchema<
853855
* This interface was referenced by `Item`'s JSON-Schema
854856
* via the `instances.hrefSchema` link.
855857
*/
856-
export type ItemInstancesHrefSchema = {
858+
export type ItemInstancesHrefSchema<
859+
D extends ItemTypeDefinition = ItemTypeDefinition,
860+
> = {
857861
/**
858862
* For Modular Content, Structured Text and Single Block fields. If set, returns full payload for nested blocks instead of IDs
859863
*/
@@ -877,9 +881,7 @@ export type ItemInstancesHrefSchema = {
877881
/**
878882
* Same as [GraphQL API records filters](/docs/content-delivery-api/filtering-records): you must use square brackets to indicate nesting levels. E.g. if you wanna [filter by parent record](/docs/content-delivery-api/filtering-records#parent) in a tree of records, you must use `filter[fields][parent][eq]=<ID_VALUE>`. Use snake_case for fields names. If `locale` is defined, search within that locale. Otherwise environment's main locale will be used.
879883
*/
880-
fields?: {
881-
[k: string]: unknown;
882-
};
884+
fields?: ToItemHrefSchemaField<D>;
883885
/**
884886
* When set, only valid records are included in the results.
885887
*/
@@ -906,7 +908,7 @@ export type ItemInstancesHrefSchema = {
906908
/**
907909
* Fields used to order results. You **must** specify also `filter[type]` with one element only to be able use this option. Format: `<field_name>_(ASC|DESC)`, where `<field_name>` can be either the API key of a model's field, or one of the following meta columns: `id`, `_updated_at`, `_created_at`, `_status`, `_published_at`, `_first_published_at`, `_publication_scheduled_at`, `_unpublishing_scheduled_at`, `_is_valid`, `position` (only for sortable models). You can pass multiple comma separated rules.
908910
*/
909-
order_by?: string;
911+
order_by?: ToItemHrefSchemaOrderBy<D>;
910912
/**
911913
* Whether you want the currently published versions (`published`, default) of your records, or the latest available (`current`)
912914
*/

packages/cma-client/src/generated/RawApiTypes.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
ToItemAttributes,
1010
ToItemAttributesInNestedResponse,
1111
ToItemAttributesInRequest,
12+
ToItemHrefSchemaField,
13+
ToItemHrefSchemaOrderBy,
1214
} from '../utilities/itemDefinition';
1315

1416
export type Field = FieldStableShell;
@@ -665,7 +667,9 @@ export type BuildEventInstancesHrefSchema = {
665667
* This interface was referenced by `Item`'s JSON-Schema
666668
* via the `instances.hrefSchema` link.
667669
*/
668-
export type ItemInstancesHrefSchema = {
670+
export type ItemInstancesHrefSchema<
671+
D extends ItemTypeDefinition = ItemTypeDefinition,
672+
> = {
669673
/**
670674
* For Modular Content, Structured Text and Single Block fields. If set, returns full payload for nested blocks instead of IDs
671675
*/
@@ -689,9 +693,7 @@ export type ItemInstancesHrefSchema = {
689693
/**
690694
* Same as [GraphQL API records filters](/docs/content-delivery-api/filtering-records): you must use square brackets to indicate nesting levels. E.g. if you wanna [filter by parent record](/docs/content-delivery-api/filtering-records#parent) in a tree of records, you must use `filter[fields][parent][eq]=<ID_VALUE>`. Use snake_case for fields names. If `locale` is defined, search within that locale. Otherwise environment's main locale will be used.
691695
*/
692-
fields?: {
693-
[k: string]: unknown;
694-
};
696+
fields?: ToItemHrefSchemaField<D>;
695697
/**
696698
* When set, only valid records are included in the results.
697699
*/
@@ -718,7 +720,7 @@ export type ItemInstancesHrefSchema = {
718720
/**
719721
* Fields used to order results. You **must** specify also `filter[type]` with one element only to be able use this option. Format: `<field_name>_(ASC|DESC)`, where `<field_name>` can be either the API key of a model's field, or one of the following meta columns: `id`, `_updated_at`, `_created_at`, `_status`, `_published_at`, `_first_published_at`, `_publication_scheduled_at`, `_unpublishing_scheduled_at`, `_is_valid`, `position` (only for sortable models). You can pass multiple comma separated rules.
720722
*/
721-
order_by?: string;
723+
order_by?: ToItemHrefSchemaOrderBy<D>;
722724
/**
723725
* Whether you want the currently published versions (`published`, default) of your records, or the latest available (`current`)
724726
*/

0 commit comments

Comments
 (0)