diff --git a/report/schemas.api.md b/report/schemas.api.md index 052b0550..a1674b7c 100644 --- a/report/schemas.api.md +++ b/report/schemas.api.md @@ -104,6 +104,19 @@ export enum AnalyticsDayDataSortBy { MOST_SALES = "most_sales" } +// @alpha +export type AnyMapping = { + type: MappingType.ANY; +}; + +// @alpha +export namespace AnyMapping { + const // (undocumented) + schema: JSONSchema; + const // (undocumented) + validate: ValidateFunction; +} + // @public export type AuthChain = AuthLink[]; @@ -1180,6 +1193,29 @@ export namespace Locale { validate: ValidateFunction; } +// @alpha +export type Mapping = SingleMapping | AnyMapping | RangeMapping | MultipleMapping; + +// @alpha +export namespace Mapping { + const // (undocumented) + schema: JSONSchema; + const // (undocumented) + validate: ValidateFunction; +} + +// @alpha +export enum MappingType { + // (undocumented) + ANY = "any", + // (undocumented) + MULTIPLE = "multiple", + // (undocumented) + RANGE = "range", + // (undocumented) + SINGLE = "single" +} + // @alpha export type MerkleProof = { proof: string[]; @@ -1286,6 +1322,20 @@ export enum MintSortBy { RECENTLY_MINTED = "recently_minted" } +// @alpha +export type MultipleMapping = { + type: MappingType.MULTIPLE; + ids: string[]; +}; + +// @alpha +export namespace MultipleMapping { + const // (undocumented) + schema: JSONSchema; + const // (undocumented) + validate: ValidateFunction; +} + // @alpha export enum Network { // (undocumented) @@ -2004,6 +2054,23 @@ export namespace ProviderType { validate: ValidateFunction; } +// @alpha +export type RangeMapping = { + type: MappingType.RANGE; + from: string; + to: string; +}; + +// @alpha +export namespace RangeMapping { + const // (undocumented) + _fromLessThanOrEqualTo: KeywordDefinition; + const // (undocumented) + schema: JSONSchema; + const // (undocumented) + validate: ValidateFunction; +} + // Warning: (ae-missing-release-tag) "Rarity" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "Rarity" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2410,6 +2477,20 @@ export const sendMessage: (window: { postMessage(event: any, targetOrigin: string): any; }, type: T, payload: PreviewMessagePayload, targetOrigin?: string) => void; +// @alpha +export type SingleMapping = { + type: MappingType.SINGLE; + id: string; +}; + +// @alpha +export namespace SingleMapping { + const // (undocumented) + schema: JSONSchema; + const // (undocumented) + validate: ValidateFunction; +} + // @alpha export type Snapshots = { face256: IPFSv2; @@ -2560,6 +2641,7 @@ export type SyncDeployment = SnapshotSyncDeployment | PointerChangesSyncDeployme export type ThirdPartyProps = { merkleProof: MerkleProof; content: Record; + mappings?: Mapping[]; }; // Warning: (ae-missing-release-tag) "Trade" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2937,7 +3019,8 @@ export namespace WorldConfiguration { // src/platform/events/blockchain.ts:19:3 - (ae-forgotten-export) The symbol "BidMetadata" needs to be exported by the entry point index.d.ts // src/platform/events/blockchain.ts:60:3 - (ae-forgotten-export) The symbol "RentalMetadata" needs to be exported by the entry point index.d.ts // src/platform/item/emote/adr74/emote-data-adr74.ts:7:3 - (ae-incompatible-release-tags) The symbol "representations" is marked as @public, but its signature references "EmoteRepresentationADR74" which is marked as @alpha -// src/platform/item/third-party-props.ts:6:3 - (ae-incompatible-release-tags) The symbol "merkleProof" is marked as @public, but its signature references "MerkleProof" which is marked as @alpha +// src/platform/item/third-party-props.ts:7:3 - (ae-incompatible-release-tags) The symbol "merkleProof" is marked as @public, but its signature references "MerkleProof" which is marked as @alpha +// src/platform/item/third-party-props.ts:9:3 - (ae-incompatible-release-tags) The symbol "mappings" is marked as @public, but its signature references "Mapping" which is marked as @alpha // src/platform/scene/feature-toggles.ts:11:3 - (ae-forgotten-export) The symbol "EnabledDisabled" needs to be exported by the entry point index.d.ts // src/platform/scene/feature-toggles.ts:12:3 - (ae-forgotten-export) The symbol "PortableExperiencesToggles" needs to be exported by the entry point index.d.ts // src/platform/scene/spawn-point.ts:6:3 - (ae-forgotten-export) The symbol "SinglePosition" needs to be exported by the entry point index.d.ts diff --git a/src/platform/item/emote/emote.ts b/src/platform/item/emote/emote.ts index aee13ad7..11bb983d 100644 --- a/src/platform/item/emote/emote.ts +++ b/src/platform/item/emote/emote.ts @@ -1,4 +1,4 @@ -import { isThirdParty } from '..' +import { isThirdParty, RangeMapping } from '..' import { generateLazyValidator, JSONSchema } from '../../../validation' import { BaseItem, baseItemProperties, isBaseEmote, requiredBaseItemProps } from '../base-item' import { standardProperties, StandardProps } from '../standard-props' @@ -77,5 +77,9 @@ export namespace Emote { errors: false } - export const validate = generateLazyValidator(schema, [_isThirdPartyKeywordDef, _isBaseEmoteKeywordDef]) + export const validate = generateLazyValidator(schema, [ + _isThirdPartyKeywordDef, + _isBaseEmoteKeywordDef, + RangeMapping._fromLessThanOrEqualTo + ]) } diff --git a/src/platform/item/index.ts b/src/platform/item/index.ts index 73ad4b0f..ed6fb620 100644 --- a/src/platform/item/index.ts +++ b/src/platform/item/index.ts @@ -4,5 +4,6 @@ export { Metrics } from './metrics' export { BodyShape } from './body-shape' export { StandardProps, isStandard } from './standard-props' export { ThirdPartyProps, isThirdParty } from './third-party-props' +export * from './linked-wearable-props' export * from './wearable' export * from './emote' diff --git a/src/platform/item/linked-wearable-props.ts b/src/platform/item/linked-wearable-props.ts new file mode 100644 index 00000000..8edb736c --- /dev/null +++ b/src/platform/item/linked-wearable-props.ts @@ -0,0 +1,175 @@ +import { generateLazyValidator, JSONSchema, ValidateFunction } from '../../validation' +import { KeywordDefinition } from 'ajv' + +/** + * MappingType + * @alpha + */ +export enum MappingType { + SINGLE = 'single', + ANY = 'any', + MULTIPLE = 'multiple', + RANGE = 'range' +} + +/** + * Mapping + * @alpha + */ +export type Mapping = SingleMapping | AnyMapping | RangeMapping | MultipleMapping + +/** + * SingleMapping + * @alpha + */ +export type SingleMapping = { + type: MappingType.SINGLE + id: string +} + +/** + * AnyMapping + * @alpha + */ +export type AnyMapping = { + type: MappingType.ANY +} + +/** + * RangeMapping + * @alpha + */ +export type RangeMapping = { + type: MappingType.RANGE + from: string + to: string +} + +/** + * MultipleMapping + * @alpha + */ +export type MultipleMapping = { + type: MappingType.MULTIPLE + ids: string[] +} + +/** + * SingleMapping + * @alpha + */ +export namespace SingleMapping { + export const schema: JSONSchema = { + type: 'object', + properties: { + type: { type: 'string', const: MappingType.SINGLE }, + id: { type: 'string', pattern: '^[0-9]+$' } + }, + required: ['type', 'id'], + additionalProperties: false + } + + export const validate: ValidateFunction = generateLazyValidator(schema) +} + +/** + * AnyMapping + * @alpha + */ +export namespace AnyMapping { + export const schema: JSONSchema = { + type: 'object', + properties: { + type: { type: 'string', const: MappingType.ANY } + }, + required: ['type'], + additionalProperties: false + } + + export const validate: ValidateFunction = generateLazyValidator(schema) +} + +/** + * RangeMapping + * @alpha + */ +export namespace RangeMapping { + export const _fromLessThanOrEqualTo: KeywordDefinition = { + keyword: '_fromLessThanOrEqualTo', + validate: function validate(schema: boolean, data: any) { + if (!data || !data.from || !data.to) { + return false + } + + let { to, from } = data + if (typeof from !== 'bigint' || typeof to !== 'bigint') { + from = BigInt(from) + to = BigInt(to) + } + + return from <= to + }, + errors: false + } + + export const schema: JSONSchema = { + type: 'object', + properties: { + type: { type: 'string', const: MappingType.RANGE }, + from: { type: 'string', pattern: '^[0-9]+$' }, + to: { type: 'string', pattern: '^[0-9]+$' } + }, + required: ['type', 'from', 'to'], + additionalProperties: false, + _fromLessThanOrEqualTo: true + } + + export const validate: ValidateFunction = generateLazyValidator(schema, [_fromLessThanOrEqualTo]) +} + +/** + * MultipleMapping + * @alpha + */ +export namespace MultipleMapping { + export const schema: JSONSchema = { + type: 'object', + properties: { + type: { type: 'string', const: MappingType.MULTIPLE }, + ids: { + type: 'array', + items: { + type: 'string', + pattern: '^[0-9]+$' + }, + minItems: 1, + uniqueItems: true + } + }, + required: ['type', 'ids'], + additionalProperties: false + } + export const validate: ValidateFunction = generateLazyValidator(schema) +} + +/** + * Mappings + * @alpha + */ +export namespace Mapping { + export const schema: JSONSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: Object.values(MappingType) + } + }, + required: ['type'], + oneOf: [SingleMapping.schema, AnyMapping.schema, RangeMapping.schema, MultipleMapping.schema] + } + + export const validate: ValidateFunction = generateLazyValidator(schema, [ + RangeMapping._fromLessThanOrEqualTo + ]) +} diff --git a/src/platform/item/third-party-props.ts b/src/platform/item/third-party-props.ts index 690758a0..aad2f348 100644 --- a/src/platform/item/third-party-props.ts +++ b/src/platform/item/third-party-props.ts @@ -1,10 +1,12 @@ import { generateLazyValidator, JSONSchema, ValidateFunction } from '../../validation' -import { MerkleProof } from '../merkle-tree/merkle-proof' +import { MerkleProof } from '../merkle-tree' import { BaseItem } from './base-item' +import { Mapping, RangeMapping } from './linked-wearable-props' export type ThirdPartyProps = { merkleProof: MerkleProof content: Record + mappings?: Mapping[] } export const thirdPartyProps = { @@ -14,6 +16,13 @@ export const thirdPartyProps = { nullable: false, additionalProperties: { type: 'string' }, required: [] as any[] + }, + mappings: { + type: 'array', + items: Mapping.schema, + minItems: 1, + maxItems: 1, + nullable: true } } as const @@ -38,7 +47,10 @@ const _containsHashingKeys = { errors: false } -const validate: ValidateFunction = generateLazyValidator(schema, [_containsHashingKeys]) +const validate: ValidateFunction = generateLazyValidator(schema, [ + _containsHashingKeys, + RangeMapping._fromLessThanOrEqualTo +]) export function isThirdParty(item: T): item is T & ThirdPartyProps { return validate(item) diff --git a/src/platform/item/wearable/wearable.ts b/src/platform/item/wearable/wearable.ts index f83520e1..6e9cc8fe 100644 --- a/src/platform/item/wearable/wearable.ts +++ b/src/platform/item/wearable/wearable.ts @@ -5,6 +5,7 @@ import { BaseItem, baseItemProperties, isBaseAvatar, requiredBaseItemProps } fro import { StandardProps, standardProperties } from '../standard-props' import { isThirdParty, ThirdPartyProps, thirdPartyProps } from '../third-party-props' import { HideableWearableCategory } from './hideable-category' +import { RangeMapping } from '../linked-wearable-props' /** @alpha */ export type Wearable = BaseItem & { @@ -115,5 +116,9 @@ export namespace Wearable { * - merkleProof * - content */ - export const validate = generateLazyValidator(schema, [_isThirdPartyKeywordDef, _isBaseAvatarKeywordDef]) + export const validate = generateLazyValidator(schema, [ + _isThirdPartyKeywordDef, + _isBaseAvatarKeywordDef, + RangeMapping._fromLessThanOrEqualTo + ]) } diff --git a/test/platform/item/linked-wearable-props.spec.ts b/test/platform/item/linked-wearable-props.spec.ts new file mode 100644 index 00000000..db927a21 --- /dev/null +++ b/test/platform/item/linked-wearable-props.spec.ts @@ -0,0 +1,73 @@ +import expect from 'expect' +import { testTypeSignature } from '../../test-utils' +import { AnyMapping, Mapping, MultipleMapping, RangeMapping, SingleMapping } from '../../../src' +import { MappingType } from '../../../src' + +const singleMapping: SingleMapping = { + type: MappingType.SINGLE, + id: '1' +} + +const allMapping: AnyMapping = { + type: MappingType.ANY +} + +const multipleMapping: MultipleMapping = { + type: MappingType.MULTIPLE, + ids: ['1', '3'] +} + +const rangeMapping: RangeMapping = { + type: MappingType.RANGE, + from: '1', + to: '9' +} + +describe('Linked Wearable Props tests', () => { + testTypeSignature(Mapping, singleMapping) + + it('static tests must pass', () => { + expect(Mapping.validate(singleMapping)).toEqual(true) + expect(Mapping.validate(null)).toEqual(false) + expect(Mapping.validate({})).toEqual(false) + }) + + describe('no extra properties allowed', () => { + const testFn = ({ mapping, expected }: { mapping: any; expected: boolean }) => + function () { + expect(Mapping.validate({ ...mapping, extra: 'extra' })).toEqual(expected) + } + + it('for type: single', testFn({ mapping: singleMapping, expected: false })) + it('for type: multiple', testFn({ mapping: multipleMapping, expected: false })) + it('for type: all', testFn({ mapping: allMapping, expected: false })) + it('for type: range', testFn({ mapping: rangeMapping, expected: false })) + }) + + it('static tests must return the correct errors when missing properties', () => { + expect(Mapping.validate({})).toEqual(false) + expect(Mapping.validate({ type: MappingType.SINGLE })).toEqual(false) + expect(Mapping.validate({ type: MappingType.MULTIPLE })).toEqual(false) + expect(Mapping.validate({ type: MappingType.RANGE })).toEqual(false) + expect(Mapping.validate({ type: MappingType.ANY })).toEqual(true) + }) + + it('range mapping with from greater than to fails validation', () => { + expect( + Mapping.validate({ + type: MappingType.RANGE, + from: '11', + to: '1' + }) + ).toEqual(false) + }) + + it('multiple mapping with duplicate ids fails validation', () => { + expect( + Mapping.validate({ + type: MappingType.MULTIPLE, + ids: ['1', '3', '1'] + }) + ).toEqual(false) + }) +}) diff --git a/test/platform/item/wearable/linked-wearable.spec.ts b/test/platform/item/wearable/linked-wearable.spec.ts new file mode 100644 index 00000000..a483dc88 --- /dev/null +++ b/test/platform/item/wearable/linked-wearable.spec.ts @@ -0,0 +1,148 @@ +import expect from 'expect' +import { + BodyPartCategory, + BodyShape, + isThirdParty, + Locale, + ThirdPartyProps, + Wearable, + WearableCategory, + WearableRepresentation +} from '../../../../src' +import { expectValidationFailureWithErrors, testTypeSignature } from '../../../test-utils' +import { MappingType } from '../../../../src' + +describe('Linked wearables tests', () => { + const representation: WearableRepresentation = { + bodyShapes: [BodyShape.FEMALE], + mainFile: 'file1', + contents: ['file1', 'file2'], + overrideHides: [WearableCategory.HAIR, BodyPartCategory.HANDS], + overrideReplaces: [BodyPartCategory.HANDS, WearableCategory.EYEWEAR] + } + + const baseWearable = { + id: 'some id', + name: 'name', + description: 'some description', + data: { + replaces: [WearableCategory.EYES, BodyPartCategory.HEAD], + hides: [WearableCategory.EYEBROWS, BodyPartCategory.HANDS], + tags: ['tag1'], + representations: [representation], + category: WearableCategory.UPPER_BODY, + blockVrmExport: false + }, + i18n: [ + { + code: Locale.EN, + text: 'some id' + } + ], + thumbnail: 'thumbnail.png', + image: 'image.png' + } + + const thirdParty: ThirdPartyProps = { + content: { + 'thumbnail.png': 'someHash', + 'iamge.png': 'someOtherHash' + }, + merkleProof: { + index: 61575, + proof: [ + '0xc8ae2407cffddd38e3bcb6c6f021c9e7ac21fcc60be44e76e4afcb34f637d562', + '0x16123d205a70cdeff7643de64cdc69a0517335d9c843479e083fd444ea823172', + '0x1fbe73f1e71f11fb4e88de5404f3177673bdfc89e93d9a496849b4ed32c9b04f', + '0xed60c527e6774dbf6750f7e28dbf93c25a22660085f709c3a0a772606768fd91', + '0x7aff1c982d6a98544c126a0676ac98102533072b6c4506f31b413757e38f4c30', + '0x5f5170cdf5fdd7bb25c225d08b48361e41f05477880812f7f5954e75daa6c667', + '0x08ae25d236fa4105b2c5136938bc42f55d339f8e4d9feb776799681b8a8a48e7', + '0xadfcc425df780be50983856c7de4d405a3ec054b74020628a9d13fdbaff35df7', + '0xda4ee1c4148a25eefbef12a92cc6a754c6312c1ff15c059f46e049ca4e5ca43b', + '0x98c363c32c7b1d7914332efaa19ad2bee7e110d79d7690650dbe7ce8ba1002a2', + '0x0bd810301fbafeb4848f7b60a378c9017a452286836d19a108812682edf8a12a', + '0x1533c6b3879f90b92fc97ec9a1db86f201623481b1e0dc0eefa387584c5d93da', + '0x31c2c3dbf88646a964edd88edb864b536182619a02905eaac2a00b0c5a6ae207', + '0xc2088dbbecba4f7dd06c689b7c1a1e6a822d20d4665b2f9353715fc3a5f0d588', + '0x9e191109e34d166ac72033dce274a82c488721a274087ae97b62c9a51944e86f', + '0x5ff2905107fe4cce21c93504414d9548f311cd27efe5696c0e03acc059d2e445', + '0x6c764a5d8ded16bf0b04028b5754afbd216b111fa0c9b10f2126ac2e9002e2fa' + ], + hashingKeys: ['id', 'name', 'description', 'i18n', 'image', 'thumbnail', 'data', 'content', 'mappings'], + entityHash: '52c312f5e5524739388af971cddb526c3b49ba31ec77abc07ca01f5b113f1eba' + }, + mappings: [ + { + type: MappingType.SINGLE, + id: '0' + } + ] + } + + const linkedWearable = { ...baseWearable, ...thirdParty } + + testTypeSignature(Wearable, linkedWearable) + + it('static base wearable must pass', () => { + expect( + Wearable.validate({ + ...baseWearable, + id: 'urn:decentraland:off-chain:base-avatars:basemale' + }) + ).toEqual(true) + }) + + it('static tests must pass', () => { + expect(Wearable.validate(linkedWearable)).toEqual(true) + expect(Wearable.validate(null)).toEqual(false) + expect(Wearable.validate({})).toEqual(false) + }) + + it('static tests must return the correct errors when missing properties', () => { + expectValidationFailureWithErrors(Wearable.validate, {}, [ + "must have required property 'id'", + "must have required property 'description'", + "must have required property 'name'", + "must have required property 'i18n'", + "must have required property 'thumbnail'", + "must have required property 'image'" + ]) + }) + + it('wearable with thirdparty props is thirdParty', () => { + expect(isThirdParty(linkedWearable)).toBeTruthy() + }) + + it('wearable with invalid mappings fails', () => { + expectValidationFailureWithErrors( + Wearable.validate, + { + ...linkedWearable, + mappings: [] + }, + ['must NOT have fewer than 1 items'] + ) + }) + + // For now we will support only one mapping type + it('wearable with more than one mapping fails', () => { + expectValidationFailureWithErrors( + Wearable.validate, + { + ...linkedWearable, + mappings: [ + { + type: MappingType.MULTIPLE, + ids: [1, 2] + }, + { + type: MappingType.SINGLE, + id: 3 + } + ] + }, + ['must NOT have more than 1 items'] + ) + }) +}) diff --git a/test/platform/item/wearable/wearable.spec.ts b/test/platform/item/wearable/wearable.spec.ts index f9e61213..dd3c1ca5 100644 --- a/test/platform/item/wearable/wearable.spec.ts +++ b/test/platform/item/wearable/wearable.spec.ts @@ -1,8 +1,8 @@ import expect from 'expect' import { Rarity, WearableCategory } from '../../../../src' -import { BodyShape, Wearable, WearableRepresentation, Locale, BodyPartCategory } from '../../../../src/platform' -import { isStandard } from '../../../../src/platform/item/standard-props' -import { isThirdParty } from '../../../../src/platform/item/third-party-props' +import { BodyShape, Wearable, WearableRepresentation, Locale, BodyPartCategory } from '../../../../src' +import { isStandard } from '../../../../src' +import { isThirdParty } from '../../../../src' import { expectValidationFailureWithErrors, testTypeSignature } from '../../../test-utils' describe('Wearable representation tests', () => { diff --git a/test/platform/notifications/subscription-details.spec.ts b/test/platform/notifications/subscription-details.spec.ts index b7056377..de1a1b0f 100644 --- a/test/platform/notifications/subscription-details.spec.ts +++ b/test/platform/notifications/subscription-details.spec.ts @@ -52,7 +52,6 @@ describe('Subscription details tests', () => { }, schemaPath: '#/properties/message_type/required' })) - console.log(errors) expect(SubscriptionDetails.validate.errors).toEqual(errors) })