Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for alternative ID types #2622

Merged
merged 12 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"lint": "eslint packages --ext .ts",
"test": "TZ=utc jest --coverage",
"test:ci": "TZ=utc jest --testRegex='.*\\.(spec|test)\\.ts$'",
"test:all": "TZ=utc node --expose-gc ./node_modules/.bin/jest --logHeapUsage --testRegex='.*\\.(spec|test)\\.ts$' --forceExit --ci -w=2 --clearMocks",
"test:docker": "docker-compose -f test/docker-compose.yaml up --remove-orphans --abort-on-container-exit --build test",
"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",
"test:docker": "docker compose -f test/docker-compose.yaml up --remove-orphans --abort-on-container-exit --build test",
"postinstall": "husky install"
},
"lint-staged": {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Changed
- Updated codegen to support id types other than string (#2622)

### Fixed
- Missing chalk dependency (#2622)

## [5.3.3] - 2024-12-04
### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@subql/common": "workspace:*",
"@subql/utils": "workspace:*",
"boxen": "5.1.2",
"chalk": "^4",
"ejs": "^3.1.10",
"fs-extra": "^11.2.0",
"fuzzy": "^0.1.3",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/controller/codegen-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
return Object.keys(plainManifest)
.filter((key) => !expectKeys.includes(key))
.map((dsKey) => {
const value = (plainManifest as any)[dsKey];

Check warning on line 206 in packages/cli/src/controller/codegen-controller.ts

View workflow job for this annotation

GitHub Actions / code-style

Unexpected any. Specify a different type
if (typeof value === 'object' && value) {
return !!Object.keys(value).find((d) => d === 'assets') && value;
}
Expand Down Expand Up @@ -297,6 +297,7 @@
const entityName = validateEntityName(entity.name);

const fields = processFields('entity', className, entity.fields, entity.indexes);
const idType = fields.find((f) => f.name === 'id')?.type ?? 'string';
const importJsonInterfaces = uniq(fields.filter((field) => field.isJsonInterface).map((f) => f.type));
const importEnums = uniq(fields.filter((field) => field.isEnum).map((f) => f.type));
const indexedFields = fields.filter((field) => field.indexed && !field.isJsonInterface);
Expand All @@ -309,6 +310,7 @@
importJsonInterfaces,
importEnums,
indexedFields,
idType,
},
helper: {
upperFirst,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/controller/init-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ export async function cloneProjectTemplate(
const tempPath = await makeTempDir();
//use sparse-checkout to clone project to temp directory
await git(tempPath).init().addRemote('origin', selectedProject.remote);
await git(tempPath).raw('sparse-checkout', 'set', `${selectedProject.path}`);
await git(tempPath).raw('sparse-checkout', 'set', selectedProject.path);
await git(tempPath).raw('pull', 'origin', 'main');
// Copy content to project path
copySync(path.join(tempPath, `${selectedProject.path}`), projectPath);
copySync(path.join(tempPath, selectedProject.path), projectPath);
// Clean temp folder
fs.rmSync(tempPath, {recursive: true, force: true});
return projectPath;
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/createProject.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ async function getExampleProject(networkFamily: string, network: string): Promis
code: string;
networks: {code: string; examples: ExampleProjectInterface[]}[];
}[];
const template = templates.find((t) => t.code === networkFamily)?.networks.find((n) => n.code === network)
?.examples[0];
const template = templates
.find((t) => t.code === networkFamily)
?.networks.find((n) => n.code === network)
?.examples.find((e) => e.remote === 'https://github.com/subquery/subql-starter');
assert(template, 'Failed to get template');
return template;
}
Expand Down
36 changes: 21 additions & 15 deletions packages/cli/src/template/model.ts.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import {<% props.importEnums.forEach(function(e){ %>

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

export class <%= props.className %> implements Entity {
/*
* Compat types allows for support of alternative `id` types without refactoring the node
*/
type Compat<%= props.className %>Props = Omit<<%= props.className %>Props, 'id'> & { id: string; };
type CompatEntity = Omit<Entity, 'id'> & { id: <%=props.idType %>; };

export class <%= props.className %> implements CompatEntity {

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

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

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

static async get(id: string): Promise<<%=props.className %> | undefined> {
static async get(id: <%=props.idType %>): Promise<<%=props.className %> | undefined> {
assert((id !== null && id !== undefined), "Cannot get <%=props.className %> entity without an ID");
const record = await store.get('<%=props.entityName %>', id.toString());
if (record) {
return this.create(record as <%= props.className %>Props);
return this.create(record as unknown as <%= props.className %>Props);
} else {
return;
}
Expand All @@ -56,14 +62,14 @@ export class <%= props.className %> implements Entity {
static async getBy<%=helper.upperFirst(field.name) %>(<%=field.name %>: <%=field.type %>): Promise<<%=props.className %> | undefined> {
const record = await store.getOneByField('<%=props.entityName %>', '<%=field.name %>', <%=field.name %>);
if (record) {
return this.create(record as <%= props.className %>Props);
return this.create(record as unknown as <%= props.className %>Props);
} else {
return;
}
}
<% } else { %>static async getBy<%=helper.upperFirst(field.name) %>(<%=field.name %>: <%=field.type %>, options: GetOptions<<%=props.className %>>): Promise<<%=props.className %>[]> {
const records = await store.getByField<<%=props.className %>>('<%=props.entityName %>', '<%=field.name %>', <%=field.name %>, options);
return records.map(record => this.create(record as <%= props.className %>Props));
<% } else { %>static async getBy<%=helper.upperFirst(field.name) %>(<%=field.name %>: <%=field.type %>, options: GetOptions<Compat<%=props.className %>Props>): Promise<<%=props.className %>[]> {
const records = await store.getByField<Compat<%=props.className %>Props>('<%=props.entityName %>', '<%=field.name %>', <%=field.name %>, options);
return records.map(record => this.create(record as unknown as <%= props.className %>Props));
}
<% }%>
<% }); %>
Expand All @@ -73,14 +79,14 @@ export class <%= props.className %> implements Entity {
*
* ⚠️ This function will first search cache data followed by DB data. Please consider this when using order and offset options.⚠️
* */
static async getByFields(filter: FieldsExpression<<%= props.className %>Props>[], options: GetOptions<<%= props.className %>Props>): Promise<<%=props.className %>[]> {
const records = await store.getByFields<<%=props.className %>>('<%=props.entityName %>', filter, options);
return records.map(record => this.create(record as <%= props.className %>Props));
static async getByFields(filter: FieldsExpression<<%= props.className %>Props>[], options: GetOptions<Compat<%= props.className %>Props>): Promise<<%=props.className %>[]> {
const records = await store.getByFields<Compat<%=props.className %>Props>('<%=props.entityName %>', filter, options);
return records.map(record => this.create(record as unknown as <%= props.className %>Props));
}

static create(record: <%= props.className %>Props): <%=props.className %> {
assert(typeof record.id === 'string', "id must be provided");
let entity = new this(
assert(record.id !== undefined && record.id !== null, "id must be provided");
const entity = new this(
<% props.fields.filter(function(field) {return field.required === true;}).forEach(function(requiredField) { %> record.<%= requiredField.name %>,
<% }) %>);
Object.assign(entity,record);
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/build/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"scripts": {
"build": "subql build",
"codegen": "subql codegen",
"start:docker": "docker-compose pull && docker-compose up --remove-orphans",
"dev": "subql codegen && subql build && docker-compose pull && docker-compose up --remove-orphans",
"start:docker": "docker compose pull && docker compose up --remove-orphans",
"dev": "subql codegen && subql build && docker compose pull && docker compose up --remove-orphans",
"prepack": "rm -rf dist && npm run build",
"test": "subql build && subql-node test"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/schemaTest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"scripts": {
"build": "subql build",
"codegen": "subql codegen",
"start:docker": "docker-compose pull && docker-compose up --remove-orphans",
"dev": "subql codegen && subql build && docker-compose pull && docker-compose up --remove-orphans",
"start:docker": "docker compose pull && docker compose up --remove-orphans",
"dev": "subql codegen && subql build && docker compose pull && docker compose up --remove-orphans",
"prepack": "rm -rf dist && npm run build",
"test": "subql build && subql-node-ethereum test"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- Missing form-data dependency (#2622)

## [5.2.1] - 2024-11-25
### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"axios": "^0.28.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"form-data": "^4.0.1",
"js-yaml": "^4.1.0",
"reflect-metadata": "^0.1.14",
"semver": "^7.6.3",
Expand Down
2 changes: 2 additions & 0 deletions packages/query/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Support for ordering with fulltext search (#2623)

## [2.18.0] - 2024-12-04
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- @dbType graphql directive (#2622)

## [2.16.0] - 2024-11-25
### Changed
Expand Down
37 changes: 34 additions & 3 deletions packages/utils/src/graphql/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
BooleanValueNode,
ListTypeNode,
TypeNode,
GraphQLDirective,
} from 'graphql';
import {findDuplicateStringArray} from '../array';
import {Logger} from '../logger';
Expand Down Expand Up @@ -53,6 +54,16 @@ export function getAllEnums(_schema: GraphQLSchema | string): GraphQLEnumType[]
return getEnumsFromSchema(getSchema(_schema));
}

function getDirectives(schema: GraphQLSchema, names: string[]): GraphQLDirective[] {
const res: GraphQLDirective[] = [];
for (const name of names) {
const directive = schema.getDirective(name);
assert(directive, `${name} directive is required`);
res.push(directive);
}
return res;
}

// eslint-disable-next-line complexity
export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null): GraphQLModelsRelationsEnums {
if (_schema === null) {
Expand Down Expand Up @@ -80,9 +91,7 @@ export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null):
);

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

//If is a basic scalar type
const typeClass = getTypeByScalarName(typeString);
Expand Down Expand Up @@ -217,6 +227,27 @@ export function getAllEntitiesRelations(_schema: GraphQLSchema | string | null):
throw new Error(`index can not be added on field ${field.name}`);
}
}

// Update id type if directive specified
if (dbTypeDirectiveVal) {
if (typeString !== 'ID') {
throw new Error(`dbType directive can only be added on 'id' field, received: ${field.name}`);
}

const dbType = dbTypeDirectiveVal.type;
const t = getTypeByScalarName(dbType);

// Allowlist of types that can be used.
if (!t || !['BigInt', 'Float', 'ID', 'Int', 'String'].includes(t.name)) {
throw new Error(`${dbType} is not a defined scalar type, please use another type in the dbType directive`);
}

const f = newModel.fields.find((f) => f.name === 'id');
if (!f) {
throw new Error('Expected id field to exist on model');
}
f.type = t.name;
}
}

// Composite Indexes
Expand Down
52 changes: 52 additions & 0 deletions packages/utils/src/graphql/graphql.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,4 +483,56 @@ describe('utils that handle schema.graphql', () => {
`Field "bananas" on entity "Fruit" is missing "derivedFrom" directive. Please also make sure "Banana" has a field of type "Fruit".`
);
});

describe('dbType directive', () => {
it('allows overriding the default ID type', () => {
const graphqlSchema = gql`
type StarterEntity @entity {
id: ID! @dbType(type: "Int")
}
`;

const schema = buildSchemaFromDocumentNode(graphqlSchema);
const entityRelations = getAllEntitiesRelations(schema);
const model = entityRelations.models.find((m) => m.name === 'StarterEntity');

expect(model).toBeDefined();
expect(model?.fields[0].type).toEqual('Int');
});

it('doesnt allow the directive on fields other than id', () => {
const graphqlSchema = gql`
type StarterEntity @entity {
id: ID!
field1: Date @dbType(type: "Int")
}
`;

const schema = buildSchemaFromDocumentNode(graphqlSchema);
expect(() => getAllEntitiesRelations(schema)).toThrow(
`dbType directive can only be added on 'id' field, received: field1`
);
});

it('only allows predefined ID db types', () => {
const makeSchema = (type: string) =>
buildSchemaFromDocumentNode(gql`
type StarterEntity @entity {
id: ID! @dbType(type: "${type}")
}
`);

for (const type of ['BigInt', 'Int', 'Float', 'ID', 'String']) {
const schema = makeSchema(type);
expect(() => getAllEntitiesRelations(schema)).not.toThrow();
}

for (const type of ['JSON', 'Date', 'Bytes', 'Boolean', 'StarterEntity']) {
const schema = makeSchema(type);
expect(() => getAllEntitiesRelations(schema)).toThrow(
`${type} is not a defined scalar type, please use another type in the dbType directive`
);
}
});
});
});
1 change: 1 addition & 0 deletions packages/utils/src/graphql/schema/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const directives = gql`
directive @index(unique: Boolean) on FIELD_DEFINITION
directive @compositeIndexes(fields: [[String]]!) on OBJECT
directive @fullText(fields: [String!], language: String) on OBJECT
directive @dbType(type: String!) on FIELD_DEFINITION
`;
3 changes: 1 addition & 2 deletions test/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: '3'
version: "3"

services:
postgres:
Expand Down Expand Up @@ -31,4 +31,3 @@ services:
command:
- yarn
- test:all

13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6610,6 +6610,7 @@ __metadata:
"@types/update-notifier": ^6
"@types/websocket": ^1
boxen: 5.1.2
chalk: ^4
ejs: ^3.1.10
eslint: ^8.8.0
eslint-config-oclif: ^4.0.0
Expand Down Expand Up @@ -6773,6 +6774,7 @@ __metadata:
axios: ^0.28.0
class-transformer: ^0.5.1
class-validator: ^0.14.1
form-data: ^4.0.1
js-yaml: ^4.1.0
reflect-metadata: ^0.1.14
semver: ^7.6.3
Expand Down Expand Up @@ -12838,6 +12840,17 @@ __metadata:
languageName: node
linkType: hard

"form-data@npm:^4.0.1":
version: 4.0.1
resolution: "form-data@npm:4.0.1"
dependencies:
asynckit: ^0.4.0
combined-stream: ^1.0.8
mime-types: ^2.1.12
checksum: ccee458cd5baf234d6b57f349fe9cc5f9a2ea8fd1af5ecda501a18fd1572a6dd3bf08a49f00568afd995b6a65af34cb8dec083cf9d582c4e621836499498dd84
languageName: node
linkType: hard

"forwarded@npm:0.2.0":
version: 0.2.0
resolution: "forwarded@npm:0.2.0"
Expand Down
Loading