Skip to content

Commit 9716762

Browse files
authored
Add support for alternative ID types (#2622)
* Add support for alternative ID types * Tidy up * Update changelog * Fix test * Fix missing as * Address feedback * Add missing deps * Update changelogs * drop chalk to non-esm version * Update changelog * Attempt to debug test * Address comment
1 parent ba74af8 commit 9716762

18 files changed

+149
-30
lines changed

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@
4444
"lint": "eslint packages --ext .ts",
4545
"test": "TZ=utc jest --coverage",
4646
"test:ci": "TZ=utc jest --testRegex='.*\\.(spec|test)\\.ts$'",
47-
"test:all": "TZ=utc node --expose-gc ./node_modules/.bin/jest --logHeapUsage --testRegex='.*\\.(spec|test)\\.ts$' --forceExit --ci -w=2 --clearMocks",
48-
"test:docker": "docker-compose -f test/docker-compose.yaml up --remove-orphans --abort-on-container-exit --build test",
47+
"test:all": "TZ=utc node --expose-gc ./node_modules/.bin/jest --logHeapUsage --forceExit --ci -w=2 --clearMocks packages/cli/src/controller/publish-controller.spec.ts",
48+
"test:docker": "docker compose -f test/docker-compose.yaml up --remove-orphans --abort-on-container-exit --build test",
4949
"postinstall": "husky install"
5050
},
5151
"lint-staged": {

packages/cli/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Changed
9+
- Updated codegen to support id types other than string (#2622)
10+
11+
### Fixed
12+
- Missing chalk dependency (#2622)
813

914
## [5.3.3] - 2024-12-04
1015
### Changed

packages/cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@subql/common": "workspace:*",
1414
"@subql/utils": "workspace:*",
1515
"boxen": "5.1.2",
16+
"chalk": "^4",
1617
"ejs": "^3.1.10",
1718
"fs-extra": "^11.2.0",
1819
"fuzzy": "^0.1.3",

packages/cli/src/controller/codegen-controller.ts

+2
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ export async function generateModels(projectPath: string, schema: string): Promi
297297
const entityName = validateEntityName(entity.name);
298298

299299
const fields = processFields('entity', className, entity.fields, entity.indexes);
300+
const idType = fields.find((f) => f.name === 'id')?.type ?? 'string';
300301
const importJsonInterfaces = uniq(fields.filter((field) => field.isJsonInterface).map((f) => f.type));
301302
const importEnums = uniq(fields.filter((field) => field.isEnum).map((f) => f.type));
302303
const indexedFields = fields.filter((field) => field.indexed && !field.isJsonInterface);
@@ -309,6 +310,7 @@ export async function generateModels(projectPath: string, schema: string): Promi
309310
importJsonInterfaces,
310311
importEnums,
311312
indexedFields,
313+
idType,
312314
},
313315
helper: {
314316
upperFirst,

packages/cli/src/controller/init-controller.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ export async function cloneProjectTemplate(
130130
const tempPath = await makeTempDir();
131131
//use sparse-checkout to clone project to temp directory
132132
await git(tempPath).init().addRemote('origin', selectedProject.remote);
133-
await git(tempPath).raw('sparse-checkout', 'set', `${selectedProject.path}`);
133+
await git(tempPath).raw('sparse-checkout', 'set', selectedProject.path);
134134
await git(tempPath).raw('pull', 'origin', 'main');
135135
// Copy content to project path
136-
copySync(path.join(tempPath, `${selectedProject.path}`), projectPath);
136+
copySync(path.join(tempPath, selectedProject.path), projectPath);
137137
// Clean temp folder
138138
fs.rmSync(tempPath, {recursive: true, force: true});
139139
return projectPath;

packages/cli/src/createProject.fixtures.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ async function getExampleProject(networkFamily: string, network: string): Promis
6060
code: string;
6161
networks: {code: string; examples: ExampleProjectInterface[]}[];
6262
}[];
63-
const template = templates.find((t) => t.code === networkFamily)?.networks.find((n) => n.code === network)
64-
?.examples[0];
63+
const template = templates
64+
.find((t) => t.code === networkFamily)
65+
?.networks.find((n) => n.code === network)
66+
?.examples.find((e) => e.remote === 'https://github.com/subquery/subql-starter');
6567
assert(template, 'Failed to get template');
6668
return template;
6769
}

packages/cli/src/template/model.ts.ejs

+21-15
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ import {<% props.importEnums.forEach(function(e){ %>
1313

1414
export type <%= props.className %>Props = Omit<<%=props.className %>, NonNullable<FunctionPropertyNames<<%=props.className %>>> | '_name'>;
1515

16-
export class <%= props.className %> implements Entity {
16+
/*
17+
* Compat types allows for support of alternative `id` types without refactoring the node
18+
*/
19+
type Compat<%= props.className %>Props = Omit<<%= props.className %>Props, 'id'> & { id: string; };
20+
type CompatEntity = Omit<Entity, 'id'> & { id: <%=props.idType %>; };
21+
22+
export class <%= props.className %> implements CompatEntity {
1723

1824
constructor(
1925
<% props.fields.forEach(function(field) { if (field.required) { %>
@@ -31,21 +37,21 @@ export class <%= props.className %> implements Entity {
3137
}
3238

3339
async save(): Promise<void> {
34-
let id = this.id;
40+
const id = this.id;
3541
assert(id !== null, "Cannot save <%=props.className %> entity without an ID");
36-
await store.set('<%=props.entityName %>', id.toString(), this);
42+
await store.set('<%=props.entityName %>', id.toString(), this as unknown as Compat<%=props.className %>Props);
3743
}
3844

39-
static async remove(id: string): Promise<void> {
45+
static async remove(id: <%=props.idType %>): Promise<void> {
4046
assert(id !== null, "Cannot remove <%=props.className %> entity without an ID");
4147
await store.remove('<%=props.entityName %>', id.toString());
4248
}
4349

44-
static async get(id: string): Promise<<%=props.className %> | undefined> {
50+
static async get(id: <%=props.idType %>): Promise<<%=props.className %> | undefined> {
4551
assert((id !== null && id !== undefined), "Cannot get <%=props.className %> entity without an ID");
4652
const record = await store.get('<%=props.entityName %>', id.toString());
4753
if (record) {
48-
return this.create(record as <%= props.className %>Props);
54+
return this.create(record as unknown as <%= props.className %>Props);
4955
} else {
5056
return;
5157
}
@@ -56,14 +62,14 @@ export class <%= props.className %> implements Entity {
5662
static async getBy<%=helper.upperFirst(field.name) %>(<%=field.name %>: <%=field.type %>): Promise<<%=props.className %> | undefined> {
5763
const record = await store.getOneByField('<%=props.entityName %>', '<%=field.name %>', <%=field.name %>);
5864
if (record) {
59-
return this.create(record as <%= props.className %>Props);
65+
return this.create(record as unknown as <%= props.className %>Props);
6066
} else {
6167
return;
6268
}
6369
}
64-
<% } else { %>static async getBy<%=helper.upperFirst(field.name) %>(<%=field.name %>: <%=field.type %>, options: GetOptions<<%=props.className %>>): Promise<<%=props.className %>[]> {
65-
const records = await store.getByField<<%=props.className %>>('<%=props.entityName %>', '<%=field.name %>', <%=field.name %>, options);
66-
return records.map(record => this.create(record as <%= props.className %>Props));
70+
<% } else { %>static async getBy<%=helper.upperFirst(field.name) %>(<%=field.name %>: <%=field.type %>, options: GetOptions<Compat<%=props.className %>Props>): Promise<<%=props.className %>[]> {
71+
const records = await store.getByField<Compat<%=props.className %>Props>('<%=props.entityName %>', '<%=field.name %>', <%=field.name %>, options);
72+
return records.map(record => this.create(record as unknown as <%= props.className %>Props));
6773
}
6874
<% }%>
6975
<% }); %>
@@ -73,14 +79,14 @@ export class <%= props.className %> implements Entity {
7379
*
7480
* ⚠️ This function will first search cache data followed by DB data. Please consider this when using order and offset options.⚠️
7581
* */
76-
static async getByFields(filter: FieldsExpression<<%= props.className %>Props>[], options: GetOptions<<%= props.className %>Props>): Promise<<%=props.className %>[]> {
77-
const records = await store.getByFields<<%=props.className %>>('<%=props.entityName %>', filter, options);
78-
return records.map(record => this.create(record as <%= props.className %>Props));
82+
static async getByFields(filter: FieldsExpression<<%= props.className %>Props>[], options: GetOptions<Compat<%= props.className %>Props>): Promise<<%=props.className %>[]> {
83+
const records = await store.getByFields<Compat<%=props.className %>Props>('<%=props.entityName %>', filter, options);
84+
return records.map(record => this.create(record as unknown as <%= props.className %>Props));
7985
}
8086

8187
static create(record: <%= props.className %>Props): <%=props.className %> {
82-
assert(typeof record.id === 'string', "id must be provided");
83-
let entity = new this(
88+
assert(record.id !== undefined && record.id !== null, "id must be provided");
89+
const entity = new this(
8490
<% props.fields.filter(function(field) {return field.required === true;}).forEach(function(requiredField) { %> record.<%= requiredField.name %>,
8591
<% }) %>);
8692
Object.assign(entity,record);

packages/cli/test/build/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"scripts": {
77
"build": "subql build",
88
"codegen": "subql codegen",
9-
"start:docker": "docker-compose pull && docker-compose up --remove-orphans",
10-
"dev": "subql codegen && subql build && docker-compose pull && docker-compose up --remove-orphans",
9+
"start:docker": "docker compose pull && docker compose up --remove-orphans",
10+
"dev": "subql codegen && subql build && docker compose pull && docker compose up --remove-orphans",
1111
"prepack": "rm -rf dist && npm run build",
1212
"test": "subql build && subql-node test"
1313
},

packages/cli/test/schemaTest/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"scripts": {
77
"build": "subql build",
88
"codegen": "subql codegen",
9-
"start:docker": "docker-compose pull && docker-compose up --remove-orphans",
10-
"dev": "subql codegen && subql build && docker-compose pull && docker-compose up --remove-orphans",
9+
"start:docker": "docker compose pull && docker compose up --remove-orphans",
10+
"dev": "subql codegen && subql build && docker compose pull && docker compose up --remove-orphans",
1111
"prepack": "rm -rf dist && npm run build",
1212
"test": "subql build && subql-node-ethereum test"
1313
},

packages/common/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Fixed
9+
- Missing form-data dependency (#2622)
810

911
## [5.2.1] - 2024-11-25
1012
### Changed

packages/common/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"axios": "^0.28.0",
1919
"class-transformer": "^0.5.1",
2020
"class-validator": "^0.14.1",
21+
"form-data": "^4.0.1",
2122
"js-yaml": "^4.1.0",
2223
"reflect-metadata": "^0.1.14",
2324
"semver": "^7.6.3",

packages/query/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Support for ordering with fulltext search (#2623)
810

911
## [2.18.0] - 2024-12-04
1012
### Fixed

packages/utils/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- @dbType graphql directive (#2622)
810

911
## [2.16.0] - 2024-11-25
1012
### Changed

packages/utils/src/graphql/entities.ts

+34-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
BooleanValueNode,
2323
ListTypeNode,
2424
TypeNode,
25+
GraphQLDirective,
2526
} from 'graphql';
2627
import {findDuplicateStringArray} from '../array';
2728
import {Logger} from '../logger';
@@ -53,6 +54,16 @@ export function getAllEnums(_schema: GraphQLSchema | string): GraphQLEnumType[]
5354
return getEnumsFromSchema(getSchema(_schema));
5455
}
5556

57+
function getDirectives(schema: GraphQLSchema, names: string[]): GraphQLDirective[] {
58+
const res: GraphQLDirective[] = [];
59+
for (const name of names) {
60+
const directive = schema.getDirective(name);
61+
assert(directive, `${name} directive is required`);
62+
res.push(directive);
63+
}
64+
return res;
65+
}
66+
5667
// eslint-disable-next-line complexity
5768
export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null): GraphQLModelsRelationsEnums {
5869
if (_schema === null) {
@@ -80,9 +91,7 @@ export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null):
8091
);
8192

8293
const modelRelations = {models: [], relations: [], enums: [...enums.values()]} as GraphQLModelsRelationsEnums;
83-
const derivedFrom = schema.getDirective('derivedFrom');
84-
const indexDirective = schema.getDirective('index');
85-
assert(derivedFrom && indexDirective, 'derivedFrom and index directives are required');
94+
const [derivedFrom, indexDirective, idDbType] = getDirectives(schema, ['derivedFrom', 'index', 'dbType']);
8695
for (const entity of entities) {
8796
const newModel: GraphQLModelsType = {
8897
name: entity.name,
@@ -104,6 +113,7 @@ export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null):
104113
const typeString = extractType(field.type);
105114
const derivedFromDirectValues = field.astNode ? getDirectiveValues(derivedFrom, field.astNode) : undefined;
106115
const indexDirectiveVal = field.astNode ? getDirectiveValues(indexDirective, field.astNode) : undefined;
116+
const dbTypeDirectiveVal = field.astNode ? getDirectiveValues(idDbType, field.astNode) : undefined;
107117

108118
//If is a basic scalar type
109119
const typeClass = getTypeByScalarName(typeString);
@@ -217,6 +227,27 @@ export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null):
217227
throw new Error(`index can not be added on field ${field.name}`);
218228
}
219229
}
230+
231+
// Update id type if directive specified
232+
if (dbTypeDirectiveVal) {
233+
if (typeString !== 'ID') {
234+
throw new Error(`dbType directive can only be added on 'id' field, received: ${field.name}`);
235+
}
236+
237+
const dbType = dbTypeDirectiveVal.type;
238+
const t = getTypeByScalarName(dbType);
239+
240+
// Allowlist of types that can be used.
241+
if (!t || !['BigInt', 'Float', 'ID', 'Int', 'String'].includes(t.name)) {
242+
throw new Error(`${dbType} is not a defined scalar type, please use another type in the dbType directive`);
243+
}
244+
245+
const f = newModel.fields.find((f) => f.name === 'id');
246+
if (!f) {
247+
throw new Error('Expected id field to exist on model');
248+
}
249+
f.type = t.name;
250+
}
220251
}
221252

222253
// Composite Indexes

packages/utils/src/graphql/graphql.spec.ts

+52
Original file line numberDiff line numberDiff line change
@@ -483,4 +483,56 @@ describe('utils that handle schema.graphql', () => {
483483
`Field "bananas" on entity "Fruit" is missing "derivedFrom" directive. Please also make sure "Banana" has a field of type "Fruit".`
484484
);
485485
});
486+
487+
describe('dbType directive', () => {
488+
it('allows overriding the default ID type', () => {
489+
const graphqlSchema = gql`
490+
type StarterEntity @entity {
491+
id: ID! @dbType(type: "Int")
492+
}
493+
`;
494+
495+
const schema = buildSchemaFromDocumentNode(graphqlSchema);
496+
const entityRelations = getAllEntitiesRelations(schema);
497+
const model = entityRelations.models.find((m) => m.name === 'StarterEntity');
498+
499+
expect(model).toBeDefined();
500+
expect(model?.fields[0].type).toEqual('Int');
501+
});
502+
503+
it('doesnt allow the directive on fields other than id', () => {
504+
const graphqlSchema = gql`
505+
type StarterEntity @entity {
506+
id: ID!
507+
field1: Date @dbType(type: "Int")
508+
}
509+
`;
510+
511+
const schema = buildSchemaFromDocumentNode(graphqlSchema);
512+
expect(() => getAllEntitiesRelations(schema)).toThrow(
513+
`dbType directive can only be added on 'id' field, received: field1`
514+
);
515+
});
516+
517+
it('only allows predefined ID db types', () => {
518+
const makeSchema = (type: string) =>
519+
buildSchemaFromDocumentNode(gql`
520+
type StarterEntity @entity {
521+
id: ID! @dbType(type: "${type}")
522+
}
523+
`);
524+
525+
for (const type of ['BigInt', 'Int', 'Float', 'ID', 'String']) {
526+
const schema = makeSchema(type);
527+
expect(() => getAllEntitiesRelations(schema)).not.toThrow();
528+
}
529+
530+
for (const type of ['JSON', 'Date', 'Bytes', 'Boolean', 'StarterEntity']) {
531+
const schema = makeSchema(type);
532+
expect(() => getAllEntitiesRelations(schema)).toThrow(
533+
`${type} is not a defined scalar type, please use another type in the dbType directive`
534+
);
535+
}
536+
});
537+
});
486538
});

packages/utils/src/graphql/schema/directives.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export const directives = gql`
1010
directive @index(unique: Boolean) on FIELD_DEFINITION
1111
directive @compositeIndexes(fields: [[String]]!) on OBJECT
1212
directive @fullText(fields: [String!], language: String) on OBJECT
13+
directive @dbType(type: String!) on FIELD_DEFINITION
1314
`;

test/docker-compose.yaml

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version: '3'
1+
version: "3"
22

33
services:
44
postgres:
@@ -31,4 +31,3 @@ services:
3131
command:
3232
- yarn
3333
- test:all
34-

yarn.lock

+13
Original file line numberDiff line numberDiff line change
@@ -6610,6 +6610,7 @@ __metadata:
66106610
"@types/update-notifier": ^6
66116611
"@types/websocket": ^1
66126612
boxen: 5.1.2
6613+
chalk: ^4
66136614
ejs: ^3.1.10
66146615
eslint: ^8.8.0
66156616
eslint-config-oclif: ^4.0.0
@@ -6773,6 +6774,7 @@ __metadata:
67736774
axios: ^0.28.0
67746775
class-transformer: ^0.5.1
67756776
class-validator: ^0.14.1
6777+
form-data: ^4.0.1
67766778
js-yaml: ^4.1.0
67776779
reflect-metadata: ^0.1.14
67786780
semver: ^7.6.3
@@ -12838,6 +12840,17 @@ __metadata:
1283812840
languageName: node
1283912841
linkType: hard
1284012842

12843+
"form-data@npm:^4.0.1":
12844+
version: 4.0.1
12845+
resolution: "form-data@npm:4.0.1"
12846+
dependencies:
12847+
asynckit: ^0.4.0
12848+
combined-stream: ^1.0.8
12849+
mime-types: ^2.1.12
12850+
checksum: ccee458cd5baf234d6b57f349fe9cc5f9a2ea8fd1af5ecda501a18fd1572a6dd3bf08a49f00568afd995b6a65af34cb8dec083cf9d582c4e621836499498dd84
12851+
languageName: node
12852+
linkType: hard
12853+
1284112854
"forwarded@npm:0.2.0":
1284212855
version: 0.2.0
1284312856
resolution: "forwarded@npm:0.2.0"

0 commit comments

Comments
 (0)