From dad8103cd1d20482f2fe0ee45cc67a2da936b25c Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 14 Mar 2025 16:46:40 +0100 Subject: [PATCH 1/2] allow to provide an existing id when creating an entity, image, type or property --- .changeset/shaggy-windows-camp.md | 5 +++++ src/core/image.ts | 11 ++++++++--- src/graph/create-entity.test.ts | 19 +++++++++++++++++++ src/graph/create-entity.ts | 5 +++-- src/graph/create-image.test.ts | 11 +++++++++++ src/graph/create-image.ts | 9 +++++++-- src/graph/create-property.test.ts | 12 ++++++++++++ src/graph/create-property.ts | 5 +++-- src/graph/create-type.test.ts | 11 +++++++++++ src/graph/create-type.ts | 5 +++-- src/types.ts | 1 + 11 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 .changeset/shaggy-windows-camp.md diff --git a/.changeset/shaggy-windows-camp.md b/.changeset/shaggy-windows-camp.md new file mode 100644 index 0000000..40d8518 --- /dev/null +++ b/.changeset/shaggy-windows-camp.md @@ -0,0 +1,5 @@ +--- +"@graphprotocol/grc-20": minor +--- + +allow to provide an existing id when creating an entity, image, type or property diff --git a/src/core/image.ts b/src/core/image.ts index cd55120..04a79fa 100644 --- a/src/core/image.ts +++ b/src/core/image.ts @@ -21,6 +21,7 @@ type MakeImageParams = { width: number; height: number; }; + id?: Id; }; /** @@ -28,7 +29,11 @@ type MakeImageParams = { * * @example * ```ts - * const { id, ops } = Image.make({ cid: 'https://example.com/image.png', dimensions: { width: 100, height: 100 } }); + * const { id, ops } = Image.make({ + * cid: 'https://example.com/image.png', + * dimensions: { width: 100, height: 100 }, + * id: imageId, // optional and will be generated if not provided + * }); * console.log(id); // 'gw9uTVTnJdhtczyuzBkL3X' * console.log(ops); // [...] * ``` @@ -36,8 +41,8 @@ type MakeImageParams = { * @returns id – base58 encoded v4 uuid representing the image entity: {@link MakeImageReturnType} * @returns ops – The ops for the Image entity: {@link MakeImageReturnType} */ -export function make({ cid, dimensions }: MakeImageParams): MakeImageReturnType { - const entityId = generate(); +export function make({ cid, dimensions, id }: MakeImageParams): MakeImageReturnType { + const entityId = id ?? generate(); const ops: Array = [ Relation.make({ fromId: entityId, diff --git a/src/graph/create-entity.test.ts b/src/graph/create-entity.test.ts index 0f024aa..c6c0763 100644 --- a/src/graph/create-entity.test.ts +++ b/src/graph/create-entity.test.ts @@ -310,4 +310,23 @@ describe('createEntity', () => { expect(typeof entity.id).toBe('string'); expect(entity.ops).toHaveLength(9); }); + + it('creates an entity with a provided id', async () => { + const entity = createEntity({ + id: Id('WeUPYRkhnQLmHPH4S1ioc4'), + name: 'Yummy Coffee', + }); + + expect(entity).toBeDefined(); + expect(entity.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); + expect(entity.ops).toHaveLength(1); + expect(entity.ops[0]).toMatchObject({ + type: 'SET_TRIPLE', + triple: { + attribute: NAME_PROPERTY, + entity: entity.id, + value: { type: 'TEXT', value: 'Yummy Coffee' }, + }, + }); + }); }); diff --git a/src/graph/create-entity.ts b/src/graph/create-entity.ts index f9c925b..29c8322 100644 --- a/src/graph/create-entity.ts +++ b/src/graph/create-entity.ts @@ -21,6 +21,7 @@ type CreateEntityParams = DefaultProperties & { * description: 'description of the entity', * cover: imageEntityId, * types: [typeEntityId1, typeEntityId2], + * id: entityId, // optional and will be generated if not provided * properties: { * // value property like text, number, url, time, point, checkbox * [propertyId]: { @@ -49,8 +50,8 @@ type CreateEntityParams = DefaultProperties & { * @returns – {@link CreateResult} * @throws Will throw an error if any provided ID is invalid */ -export const createEntity = ({ name, description, cover, properties, types }: CreateEntityParams): CreateResult => { - const id = generate(); +export const createEntity = ({ id: providedId, name, description, cover, properties, types }: CreateEntityParams): CreateResult => { + const id = providedId ?? generate(); const ops: Array = []; ops.push(...createDefaultProperties({ entityId: id, name, description, cover })); diff --git a/src/graph/create-image.test.ts b/src/graph/create-image.test.ts index c1672e5..1bfca12 100644 --- a/src/graph/create-image.test.ts +++ b/src/graph/create-image.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Id } from '../id.js'; import { SystemIds } from '../system-ids.js'; import { createImage } from './create-image.js'; @@ -129,6 +130,16 @@ describe('createImage', () => { } }); + it('creates an image with a provided id', async () => { + const image = await createImage({ + url: 'http://localhost:3000/image', + id: Id('WeUPYRkhnQLmHPH4S1ioc4'), + }); + + expect(image).toBeDefined(); + expect(image.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); + }); + it('throws an error if the image cannot be uploaded to IPFS', async () => { vi.spyOn(global, 'fetch').mockImplementation(url => { if (url.toString() === 'http://localhost:3000/image') { diff --git a/src/graph/create-image.ts b/src/graph/create-image.ts index c040a68..d36e620 100644 --- a/src/graph/create-image.ts +++ b/src/graph/create-image.ts @@ -1,3 +1,4 @@ +import type { Id } from '../id.js'; import { Image } from '../image.js'; import { uploadImage } from '../ipfs.js'; import type { CreateResult, Op } from '../types.js'; @@ -7,11 +8,13 @@ type CreateImageParams = blob: Blob; name?: string; description?: string; + id?: Id; } | { url: string; name?: string; description?: string; + id?: Id; }; /** @@ -25,22 +28,24 @@ type CreateImageParams = * url: 'https://example.com/image.png', * name: 'name of the image', // optional * description: 'description of the image', // optional + * id: imageId, // optional and will be generated if not provided * }); * * const { id, ops } = createImage({ * blob: new Blob(…), * name: 'name of the image', // optional * description: 'description of the image', // optional + * id: imageId, // optional and will be generated if not provided * }); * ``` * @param params – {@link CreateImageParams} * @returns – {@link CreateResult} * @throws Will throw an IpfsUploadError if the image cannot be uploaded to IPFS */ -export const createImage = async ({ name, description, ...params }: CreateImageParams): Promise => { +export const createImage = async ({ name, description, id: providedId, ...params }: CreateImageParams): Promise => { const ops: Array = []; const { cid, dimensions } = await uploadImage(params); - const { id, ops: imageOps } = Image.make({ cid, dimensions }); + const { id, ops: imageOps } = Image.make({ cid, dimensions, id: providedId }); ops.push(...imageOps); ops.push(...createDefaultProperties({ entityId: id, name, description })); diff --git a/src/graph/create-property.test.ts b/src/graph/create-property.test.ts index df23688..8fc9d29 100644 --- a/src/graph/create-property.test.ts +++ b/src/graph/create-property.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { JOB_TYPE, ROLES_PROPERTY } from '../core/ids/content.js'; +import { Id } from '../id.js'; import { createProperty } from './create-property.js'; describe('createProperty', () => { @@ -73,4 +74,15 @@ describe('createProperty', () => { expect(property.ops[4]?.type).toBe('CREATE_RELATION'); expect(property.ops[5]?.type).toBe('CREATE_RELATION'); }); + + it('creates a property with a provided id', async () => { + const property = createProperty({ + id: Id('WeUPYRkhnQLmHPH4S1ioc4'), + name: 'Price', + type: 'NUMBER', + }); + + expect(property).toBeDefined(); + expect(property.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); + }); }); diff --git a/src/graph/create-property.ts b/src/graph/create-property.ts index 706ce97..4fbdfa1 100644 --- a/src/graph/create-property.ts +++ b/src/graph/create-property.ts @@ -32,6 +32,7 @@ type CreatePropertyParams = DefaultProperties & * type: 'TEXT' * description: 'description of the property', * cover: imageEntityId, + * id: propertyId, // optional and will be generated if not provided * }); * ``` * @param params – {@link CreatePropertyParams} @@ -39,8 +40,8 @@ type CreatePropertyParams = DefaultProperties & * @throws Will throw an error if any provided ID is invalid */ export const createProperty = (params: CreatePropertyParams): CreateResult => { - const { name, description, cover } = params; - const entityId = generate(); + const { id, name, description, cover } = params; + const entityId = id ?? generate(); const ops: Op[] = []; ops.push(...createDefaultProperties({ entityId, name, description, cover })); diff --git a/src/graph/create-type.test.ts b/src/graph/create-type.test.ts index ebcfc61..5a77f2c 100644 --- a/src/graph/create-type.test.ts +++ b/src/graph/create-type.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { AUTHORS_PROPERTY, WEBSITE_PROPERTY } from '../core/ids/content.js'; import { NAME_PROPERTY, PROPERTY, SCHEMA_TYPE, TYPES_PROPERTY } from '../core/ids/system.js'; +import { Id } from '../id.js'; import { createType } from './create-type.js'; describe('createType', () => { @@ -100,4 +101,14 @@ describe('createType', () => { type: 'CREATE_RELATION', }); }); + + it('creates a type with a provided id', async () => { + const type = createType({ + id: Id('WeUPYRkhnQLmHPH4S1ioc4'), + name: 'Article', + }); + + expect(type).toBeDefined(); + expect(type.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); + }); }); diff --git a/src/graph/create-type.ts b/src/graph/create-type.ts index ae951f4..ad25464 100644 --- a/src/graph/create-type.ts +++ b/src/graph/create-type.ts @@ -18,6 +18,7 @@ type CreateTypeParams = DefaultProperties & { * name: 'name of the type', * description: 'description of the type', * cover: imageEntityId, + * id: typeId, // optional and will be generated if not provided * properties: [propertyEntityId1, propertyEntityId2], * }); * ``` @@ -25,8 +26,8 @@ type CreateTypeParams = DefaultProperties & { * @returns – {@link CreateResult} * @throws Will throw an error if any provided ID is invalid */ -export const createType = ({ name, description, cover, properties }: CreateTypeParams): CreateResult => { - const id = generate(); +export const createType = ({ id: providedId, name, description, cover, properties }: CreateTypeParams): CreateResult => { + const id = providedId ?? generate(); const ops: Op[] = []; ops.push(...createDefaultProperties({ entityId: id, name, description, cover })); diff --git a/src/types.ts b/src/types.ts index dd769c7..77bfa29 100644 --- a/src/types.ts +++ b/src/types.ts @@ -154,6 +154,7 @@ export type ProposalStatus = 'PROPOSED' | 'ACCEPTED' | 'REJECTED' | 'CANCELED' | export type GraphUri = `graph://${string}`; export type DefaultProperties = { + id?: Id; name?: string; description?: string; cover?: Id; From 1caf64f9b68573077e7863469abfa6d85e32071b Mon Sep 17 00:00:00 2001 From: Nik Graf Date: Fri, 14 Mar 2025 18:36:31 +0100 Subject: [PATCH 2/2] chore: validate ids and add source info --- src/graph/create-entity.test.ts | 5 +++++ src/graph/create-entity.ts | 3 +++ src/graph/create-image.test.ts | 5 +++++ src/graph/create-image.ts | 5 ++++- src/graph/create-property.test.ts | 5 +++++ src/graph/create-property.ts | 3 +++ src/graph/create-type.test.ts | 5 +++++ src/graph/create-type.ts | 3 +++ src/id.ts | 4 ++-- 9 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/graph/create-entity.test.ts b/src/graph/create-entity.test.ts index c6c0763..b00fb3c 100644 --- a/src/graph/create-entity.test.ts +++ b/src/graph/create-entity.test.ts @@ -329,4 +329,9 @@ describe('createEntity', () => { }, }); }); + + it('throws an error if the provided id is invalid', () => { + // @ts-expect-error - invalid id type + expect(() => createEntity({ id: 'invalid' })).toThrow('Invalid id: "invalid" for `id` in `createEntity`'); + }); }); diff --git a/src/graph/create-entity.ts b/src/graph/create-entity.ts index 29c8322..9f052a6 100644 --- a/src/graph/create-entity.ts +++ b/src/graph/create-entity.ts @@ -51,6 +51,9 @@ type CreateEntityParams = DefaultProperties & { * @throws Will throw an error if any provided ID is invalid */ export const createEntity = ({ id: providedId, name, description, cover, properties, types }: CreateEntityParams): CreateResult => { + if (providedId) { + assertValid(providedId, '`id` in `createEntity`'); + } const id = providedId ?? generate(); const ops: Array = []; diff --git a/src/graph/create-image.test.ts b/src/graph/create-image.test.ts index 1bfca12..8ed1b2c 100644 --- a/src/graph/create-image.test.ts +++ b/src/graph/create-image.test.ts @@ -140,6 +140,11 @@ describe('createImage', () => { expect(image.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); }); + it('throws an error if the provided id is invalid', () => { + // @ts-expect-error - invalid id type + expect(async () => await createImage({ id: 'invalid', url: 'http://localhost:3000/image' })).rejects.toThrow('Invalid id: "invalid" for `id` in `createImage`'); + }); + it('throws an error if the image cannot be uploaded to IPFS', async () => { vi.spyOn(global, 'fetch').mockImplementation(url => { if (url.toString() === 'http://localhost:3000/image') { diff --git a/src/graph/create-image.ts b/src/graph/create-image.ts index d36e620..0113235 100644 --- a/src/graph/create-image.ts +++ b/src/graph/create-image.ts @@ -1,4 +1,4 @@ -import type { Id } from '../id.js'; +import { type Id, assertValid } from '../id.js'; import { Image } from '../image.js'; import { uploadImage } from '../ipfs.js'; import type { CreateResult, Op } from '../types.js'; @@ -43,6 +43,9 @@ type CreateImageParams = * @throws Will throw an IpfsUploadError if the image cannot be uploaded to IPFS */ export const createImage = async ({ name, description, id: providedId, ...params }: CreateImageParams): Promise => { + if (providedId) { + assertValid(providedId, '`id` in `createImage`'); + } const ops: Array = []; const { cid, dimensions } = await uploadImage(params); const { id, ops: imageOps } = Image.make({ cid, dimensions, id: providedId }); diff --git a/src/graph/create-property.test.ts b/src/graph/create-property.test.ts index 8fc9d29..4e2255f 100644 --- a/src/graph/create-property.test.ts +++ b/src/graph/create-property.test.ts @@ -85,4 +85,9 @@ describe('createProperty', () => { expect(property).toBeDefined(); expect(property.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); }); + + it('throws an error if the provided id is invalid', async () => { + // @ts-expect-error - invalid id type + expect(() => createProperty({ id: 'invalid' })).toThrow('Invalid id: "invalid" for `id` in `createProperty`'); + }); }); diff --git a/src/graph/create-property.ts b/src/graph/create-property.ts index 4fbdfa1..9a8cc91 100644 --- a/src/graph/create-property.ts +++ b/src/graph/create-property.ts @@ -41,6 +41,9 @@ type CreatePropertyParams = DefaultProperties & */ export const createProperty = (params: CreatePropertyParams): CreateResult => { const { id, name, description, cover } = params; + if (id) { + assertValid(id, '`id` in `createProperty`'); + } const entityId = id ?? generate(); const ops: Op[] = []; diff --git a/src/graph/create-type.test.ts b/src/graph/create-type.test.ts index 5a77f2c..321ad24 100644 --- a/src/graph/create-type.test.ts +++ b/src/graph/create-type.test.ts @@ -111,4 +111,9 @@ describe('createType', () => { expect(type).toBeDefined(); expect(type.id).toBe('WeUPYRkhnQLmHPH4S1ioc4'); }); + + it('throws an error if the provided id is invalid', () => { + // @ts-expect-error - invalid id type + expect(() => createType({ id: 'invalid' })).toThrow('Invalid id: "invalid" for `id` in `createType`'); + }); }); diff --git a/src/graph/create-type.ts b/src/graph/create-type.ts index ad25464..61e19c8 100644 --- a/src/graph/create-type.ts +++ b/src/graph/create-type.ts @@ -27,6 +27,9 @@ type CreateTypeParams = DefaultProperties & { * @throws Will throw an error if any provided ID is invalid */ export const createType = ({ id: providedId, name, description, cover, properties }: CreateTypeParams): CreateResult => { + if (providedId) { + assertValid(providedId, '`id` in `createType`'); + } const id = providedId ?? generate(); const ops: Op[] = []; diff --git a/src/id.ts b/src/id.ts index ee67dc2..4a7e464 100644 --- a/src/id.ts +++ b/src/id.ts @@ -61,8 +61,8 @@ export function isValid(id: string): boolean { } } -export function assertValid(id: string) { +export function assertValid(id: string, sourceHint?: string) { if (!isValid(id)) { - throw new Error(`Invalid id: ${id}`); + throw new Error(`Invalid id: "${id}"${sourceHint ? ` for ${sourceHint}` : ''}`); } }