From 7c2860dbebd2618c9e9c3baae5cdd3a356493de7 Mon Sep 17 00:00:00 2001 From: Shree Vatsa N Date: Thu, 24 Oct 2024 05:14:40 +0530 Subject: [PATCH] schema-accounts: Add new SDK module to manage account based schema (#250) Signed-off-by: Shreevatsa N --- demo/src/dedi/registry-entries-tx.ts | 32 +- demo/src/dedi/registry-tx.ts | 32 +- packages/identifier/src/Identifier.ts | 7 +- packages/registries/package.json | 1 + packages/registries/src/Registries.chain.ts | 16 +- packages/registries/src/Registries.ts | 5 +- packages/schema-accounts/LICENSE | 201 +++++++++ packages/schema-accounts/package.json | 46 ++ .../src/SchemaAccounts.chain.ts | 417 ++++++++++++++++++ .../schema-accounts/src/SchemaAccounts.ts | 414 +++++++++++++++++ .../src/SchemaAccounts.types.ts | 371 ++++++++++++++++ packages/schema-accounts/src/index.ts | 3 + packages/schema-accounts/tsconfig.build.json | 17 + packages/schema-accounts/tsconfig.esm.json | 7 + packages/sdk/package.json | 1 + packages/sdk/src/index.ts | 1 + packages/types/src/Registries.ts | 5 +- packages/types/src/SchemaAccounts.ts | 13 + packages/types/src/index.ts | 1 + yarn.lock | 20 + 20 files changed, 1600 insertions(+), 10 deletions(-) create mode 100644 packages/schema-accounts/LICENSE create mode 100644 packages/schema-accounts/package.json create mode 100644 packages/schema-accounts/src/SchemaAccounts.chain.ts create mode 100644 packages/schema-accounts/src/SchemaAccounts.ts create mode 100644 packages/schema-accounts/src/SchemaAccounts.types.ts create mode 100644 packages/schema-accounts/src/index.ts create mode 100644 packages/schema-accounts/tsconfig.build.json create mode 100644 packages/schema-accounts/tsconfig.esm.json create mode 100644 packages/types/src/SchemaAccounts.ts diff --git a/demo/src/dedi/registry-entries-tx.ts b/demo/src/dedi/registry-entries-tx.ts index 732ee9dc..c7dc435c 100644 --- a/demo/src/dedi/registry-entries-tx.ts +++ b/demo/src/dedi/registry-entries-tx.ts @@ -38,6 +38,36 @@ async function main() { let tx = await api.tx.balances.transferAllowDeath(authorIdentity.address, new BN('1000000000000000')); await Cord.Chain.signAndSubmitTx(tx, authorityAuthorIdentity); + // Create a Schema + console.log(`\n❄️ Schema Creation `) + let newSchemaContent = require('../../res/schema.json') + let newSchemaName = newSchemaContent.title + ':' + Cord.Utils.UUID.generate() + newSchemaContent.title = newSchemaName + + let schemaProperties = Cord.SchemaAccounts.buildFromProperties( + newSchemaContent, + authorIdentity.address, + ) + console.dir(schemaProperties, { + depth: null, + colors: true, + }) + const schemaUri = await Cord.SchemaAccounts.dispatchToChain( + schemaProperties.schema, + authorIdentity, + ) + console.log(`✅ Schema - ${schemaUri} - added!`) + + console.log(`\n❄️ Query From Chain - Schema `) + const schemaFromChain = await Cord.SchemaAccounts.fetchFromChain( + schemaProperties.schema.$id + ) + console.dir(schemaFromChain, { + depth: null, + colors: true, + }) + console.log('✅ Schema Functions Completed!') + // Create a Registry. const blob = { "name": "Companies Registry", @@ -76,7 +106,7 @@ async function main() { const registryDetails = await Cord.Registries.registryCreateProperties( authorIdentity.address, digest, //digest - null, //schemaId + schemaUri, //schemaUri blob, //blob ); diff --git a/demo/src/dedi/registry-tx.ts b/demo/src/dedi/registry-tx.ts index e144f452..7d190067 100644 --- a/demo/src/dedi/registry-tx.ts +++ b/demo/src/dedi/registry-tx.ts @@ -38,6 +38,36 @@ async function main() { let tx = await api.tx.balances.transferAllowDeath(authorIdentity.address, new BN('1000000000000000')); await Cord.Chain.signAndSubmitTx(tx, authorityAuthorIdentity); + // Create a Schema + console.log(`\n❄️ Schema Creation `) + let newSchemaContent = require('../../res/schema.json') + let newSchemaName = newSchemaContent.title + ':' + Cord.Utils.UUID.generate() + newSchemaContent.title = newSchemaName + + let schemaProperties = Cord.SchemaAccounts.buildFromProperties( + newSchemaContent, + authorIdentity.address, + ) + console.dir(schemaProperties, { + depth: null, + colors: true, + }) + const schemaUri = await Cord.SchemaAccounts.dispatchToChain( + schemaProperties.schema, + authorIdentity, + ) + console.log(`✅ Schema - ${schemaUri} - added!`) + + console.log(`\n❄️ Query From Chain - Schema `) + const schemaFromChain = await Cord.SchemaAccounts.fetchFromChain( + schemaProperties.schema.$id + ) + console.dir(schemaFromChain, { + depth: null, + colors: true, + }) + console.log('✅ Schema Functions Completed!') + // Create a Registry. const blob = { "name": "Companies Registry", @@ -75,7 +105,7 @@ async function main() { const registryDetails = await Cord.Registries.registryCreateProperties( authorIdentity.address, digest, //digest - null, //schemaId + schemaUri, //schemaUri blob, //blob ); diff --git a/packages/identifier/src/Identifier.ts b/packages/identifier/src/Identifier.ts index ed8695bd..a1179c83 100644 --- a/packages/identifier/src/Identifier.ts +++ b/packages/identifier/src/Identifier.ts @@ -74,6 +74,7 @@ import { REGISTRYAUTH_PREFIX, ENTRY_IDENT, ENTRY_PREFIX, + SCHEMA_ACCOUNTS_IDENT, } from '@cord.network/types' import { SDKErrors } from '@cord.network/utils' @@ -97,6 +98,7 @@ const VALID_IDENTS = new Set([ REGISTRY_IDENT, REGISTRYAUTH_IDENT, ENTRY_IDENT, + SCHEMA_ACCOUNTS_IDENT, ]) const VALID_PREFIXES = [ @@ -109,7 +111,7 @@ const VALID_PREFIXES = [ ASSET_PREFIX, REGISTRY_PREFIX, REGISTRYAUTH_PREFIX, - ENTRY_PREFIX + ENTRY_PREFIX, ] const IDENT_TO_PREFIX_MAP = new Map([ @@ -123,7 +125,8 @@ const IDENT_TO_PREFIX_MAP = new Map([ [ASSET_INSTANCE_IDENT, ASSET_PREFIX], [REGISTRY_IDENT, REGISTRY_PREFIX], [REGISTRYAUTH_IDENT, REGISTRYAUTH_PREFIX], - [ENTRY_IDENT, ENTRY_PREFIX] + [ENTRY_IDENT, ENTRY_PREFIX], + [SCHEMA_ACCOUNTS_IDENT, SCHEMA_PREFIX] ]) /** diff --git a/packages/registries/package.json b/packages/registries/package.json index ab3d5b51..a1036597 100644 --- a/packages/registries/package.json +++ b/packages/registries/package.json @@ -36,6 +36,7 @@ "@cord.network/config": "workspace:*", "@cord.network/identifier": "workspace:*", "@cord.network/network": "workspace:*", + "@cord.network/schema-accounts": "workspace:*", "@cord.network/types": "workspace:*", "@cord.network/utils": "workspace:*" } diff --git a/packages/registries/src/Registries.chain.ts b/packages/registries/src/Registries.chain.ts index 0ad0a648..2331eb8d 100644 --- a/packages/registries/src/Registries.chain.ts +++ b/packages/registries/src/Registries.chain.ts @@ -76,6 +76,8 @@ import { SDKErrors } from '@cord.network/utils' import { ConfigService } from '@cord.network/config' +import { doesSchemaIdExists } from '@cord.network/schema-accounts'; + import { IRegistryCreate, IRegistryUpdate, RegistryAuthorizationUri, @@ -196,6 +198,18 @@ export async function dispatchCreateRegistryToChain( authorizationUri: registryDetails.authorizationUri } + let schemaId = null; + if (registryDetails.schemaUri) { + const schemaExists = await doesSchemaIdExists(registryDetails.schemaUri); + if (!schemaExists) { + throw new SDKErrors.CordDispatchError( + `Schema does not exists at URI: "${registryDetails.schemaUri}".` + ); + } + + schemaId = uriToIdentifier(registryDetails.schemaUri); + } + const registryExists = await isRegistryStored(registryDetails.uri); if (registryExists) { @@ -211,7 +225,7 @@ export async function dispatchCreateRegistryToChain( const extrinsic = api.tx.registries.create( registryId, registryDetails.digest, - registryDetails.schemaId, + schemaId, registryDetails.blob ); diff --git a/packages/registries/src/Registries.ts b/packages/registries/src/Registries.ts index 7cf4b523..53432571 100644 --- a/packages/registries/src/Registries.ts +++ b/packages/registries/src/Registries.ts @@ -72,6 +72,7 @@ import type { RegistryDigest, RegistryAuthorizationUri, RegistryUri, + SchemaUri } from '@cord.network/types'; import { @@ -392,7 +393,7 @@ export async function decodeCborToStringifiedBlob( export async function registryCreateProperties( creatorAddress: string, digest: HexString | null = null, - schemaId: string | null = null, + schemaUri: SchemaUri | null = null, blob: string | null = null, ): Promise { @@ -446,7 +447,7 @@ export async function registryCreateProperties( creatorUri, digest, blob, - schemaId, + schemaUri, authorizationUri, } } diff --git a/packages/schema-accounts/LICENSE b/packages/schema-accounts/LICENSE new file mode 100644 index 00000000..b5dd1bd8 --- /dev/null +++ b/packages/schema-accounts/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2024, Dhiway Networks Private Limited + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/schema-accounts/package.json b/packages/schema-accounts/package.json new file mode 100644 index 00000000..a392a4eb --- /dev/null +++ b/packages/schema-accounts/package.json @@ -0,0 +1,46 @@ +{ + "name": "@cord.network/schema-accounts", + "version": "0.9.3-1rc4", + "description": "Accounts Based Schema Repository Management", + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/cjs/index.d.ts", + "exports": { + ".": { + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js" + } + }, + "files": [ + "lib/**/*" + ], + "scripts": { + "clean": "rimraf ./lib", + "build": "yarn clean && yarn build:ts", + "build:ts": "yarn build:cjs && yarn build:esm", + "build:cjs": "tsc --declaration -p tsconfig.build.json && echo '{\"type\":\"commonjs\"}' > ./lib/cjs/package.json", + "build:esm": "tsc --declaration -p tsconfig.esm.json && echo '{\"type\":\"module\"}' > ./lib/esm/package.json" + }, + "repository": "github:dhiway/cord.js", + "engines": { + "node": ">=20.0" + }, + "author": "", + "bugs": "https://github.com/dhiway/cord.js/issues", + "homepage": "https://github.com/dhiway/cord.js#readme", + "devDependencies": { + "@types/uuid": "^9.0.8", + "rimraf": "^5.0.5", + "testcontainers": "^10.7.1", + "typescript": "^5.3.3" + }, + "dependencies": { + "@cord.network/augment-api": "workspace:*", + "@cord.network/config": "workspace:*", + "@cord.network/did": "workspace:*", + "@cord.network/identifier": "workspace:*", + "@cord.network/network": "workspace:*", + "@cord.network/types": "workspace:*", + "@cord.network/utils": "workspace:*" + } +} diff --git a/packages/schema-accounts/src/SchemaAccounts.chain.ts b/packages/schema-accounts/src/SchemaAccounts.chain.ts new file mode 100644 index 00000000..58734034 --- /dev/null +++ b/packages/schema-accounts/src/SchemaAccounts.chain.ts @@ -0,0 +1,417 @@ +/** + * @packageDocumentation + * @module SchemaAccounts/Chain + * + * This file contains functions and types related to the interaction between the schema definitions and the blockchain. + * It primarily deals with encoding and decoding schema data for blockchain storage and retrieval, as well as + * validating and verifying schema integrity in the context of blockchain interactions. + * + * The `SchemaAccountsChain` module serves as a bridge between the account based schema definitions used within the application and their + * representation on the blockchain. It provides functionalities to: + * - Convert schema objects to a format suitable for blockchain storage (`toChain`). + * - Fetch schema data from the blockchain and reconstruct it into usable schema objects (`fetchFromChain`, `fromChain`). + * - Verify the existence and integrity of schemas on the blockchain (`isSchemaStored`). + * - Generate and validate unique identifiers for schemas based on their content and creator (`getUriForSchema`). + * + * This module is crucial for ensuring that schemas are correctly stored, retrieved, and validated in a blockchain + * environment. It encapsulates the complexities of handling blockchain-specific data encoding and decoding, allowing + * other parts of the application to interact with schema data in a more abstract and convenient manner. + * + * The functions in this module are typically used in scenarios where schemas need to be registered, updated, or + * queried from the blockchain, ensuring that the schema data remains consistent and verifiable across different + * nodes in the network. + */ + +import type { + Bytes, + Option, + CordKeyringPair, + SchemaUri, +} from '@cord.network/types' + +import type { PalletSchemaAccountsSchemaEntry } from '@cord.network/augment-api' + +import { + SchemaDigest, + ISchema, + SCHEMA_PREFIX, + SCHEMA_ACCOUNTS_IDENT, + blake2AsHex, + ISchemaAccountsDetails, + SchemaId, +} from '@cord.network/types' + +import { ConfigService } from '@cord.network/config' +import { Chain } from '@cord.network/network' +import { SDKErrors, Cbor, Crypto } from '@cord.network/utils' + +import { + hashToUri, + uriToIdentifier, +} from '@cord.network/identifier' + +import { + encodeCborSchema, + verifyDataStructure +} from './SchemaAccounts.js' + + +/** + * Checks if a given schema is stored on the blockchain. + * This function queries the blockchain to determine whether the specified schema exists in the blockchain storage. + * + * @param schema - The schema object (`ISchema`) to be checked. It must contain a valid `$id` property. + * + * @returns A promise that resolves to a boolean value. It returns `true` if the schema is stored on the blockchain, + * and `false` if it is not. + * + * @example + * ```typescript + * const stored = await isSchemaStored(mySchema); + * if (stored) { + * console.log('Schema is stored on the blockchain.'); + * } else { + * console.log('Schema is not stored on the blockchain.'); + * } + * ``` + */ +export async function isSchemaStored(schema: ISchema): Promise { + const api = ConfigService.get('api'); + const identifier = uriToIdentifier(schema.$id); + const encoded = await api.query.schemaAccounts.schemas(identifier); + + return !encoded.isNone +} + + +/** + * Asynchronously checks if a schema with the given URI exists on-chain. + * + * This function interacts with the blockchain using the configured API service to verify + * the existence of a schema by querying the `schemaAccounts` storage. It converts the + * provided schema URI into an identifier, which is used to fetch the corresponding + * schema entry. If an entry exists, the function returns `true`; otherwise, it returns `false`. + * + * ### Parameters: + * @param schemaUri - The URI of the schema to be checked. This URI serves as the reference + * to uniquely identify the schema on-chain. + * + * ### Returns: + * @returns {Promise} - A promise that resolves to: + * - **`true`** if the schema exists on-chain. + * - **`false`** if the schema does not exist or the query returns `None`. + * + * ### Example Usage: + * ```typescript + * const schemaUri = 'cord:schema:123456789'; + * + * doesSchemaIdExists(schemaUri) + * .then(exists => { + * if (exists) { + * console.log('Schema exists on-chain.'); + * } else { + * console.log('Schema not found.'); + * } + * }) + * .catch(error => console.error('Error checking schema existence:', error)); + * ``` + * + * ### Internal Logic: + * 1. **Fetching the API**: The function retrieves the blockchain API instance using the `ConfigService`. + * 2. **Converting URI to Identifier**: The URI is converted into an identifier using `uriToIdentifier`. + * 3. **Querying Blockchain Storage**: It queries the `schemas` storage in `schemaAccounts` with the identifier. + * 4. **Checking Existence**: If the query returns `None`, the schema does not exist; otherwise, it exists. + * + * ### Throws: + * - Any error encountered while querying the blockchain API will be propagated as a rejected promise. + * + * ### Dependencies: + * - **ConfigService**: Retrieves the blockchain API instance. + * - **uriToIdentifier**: Converts schema URI into a blockchain-compatible identifier. + */ +export async function doesSchemaIdExists(schemaUri: SchemaUri): Promise { + const api = ConfigService.get('api'); + const identifier = uriToIdentifier(schemaUri); + const encoded = await api.query.schemaAccounts.schemas(identifier); + + return !encoded.isNone; +} + + +/** + * (Internal Function) - Generates a unique URI for a given schema based on its serialized content. + * + * This function ensures each schema is uniquely identified and reliably retrievable using the generated URI. + * + * ### Functionality + * - Uses CBOR encoding to serialize the schema for efficient processing. + * - Cryptographically hashes the serialized schema using `blake2` hashing to ensure uniqueness. + * - Encodes the schema with SCALE encoding for compatibility with Substrate-based systems. + * - Generates a URI from the schema’s digest using network-specific identifiers and prefixes. + * + * ### Parameters + * @param schema - The schema object or a version of it without the `$id` property. + * It must conform to the `ISchema` interface used in the Cord network. + * + * ### Returns + * @returns An object containing: + * - `uri`: A string representing the unique URI of the schema within the Cord network. + * - `digest`: A cryptographic hash of the schema's serialized content. + * + * ### Usage + * This function is used internally to register and manage schemas, ensuring that each one is uniquely + * identifiable within the Cord network using its URI. + * + * ### Throws + * @throws {Error} If any part of the URI generation process fails, such as issues with schema serialization, etc. + * + * @example + * const schema = { name: "Example Schema", properties: { id: "string" } }; + * const result = getUriForSchema(schema); + * console.log(result.uri); // Unique schema URI + */ +export function getUriForSchema( + schema: ISchema | Omit, +): { uri: SchemaUri; digest: SchemaDigest } { + const api = ConfigService.get('api') + const serializedSchema = encodeCborSchema(schema) + const digest = Crypto.hashStr(serializedSchema) + + const scaleEncodedSchema = api + .createType('Bytes', serializedSchema) + .toU8a() + + const IdDigest = blake2AsHex( + Uint8Array.from([ + ...scaleEncodedSchema + ]) + ) + const schemaUri = hashToUri( + IdDigest, + SCHEMA_ACCOUNTS_IDENT, + SCHEMA_PREFIX + ) as SchemaUri + + return { uri: schemaUri, digest } +} + + +/** + * Dispatches a schema to the blockchain for storage, ensuring its uniqueness, immutability, + * and verifiability. This function encodes the schema, creates a blockchain transaction, + * and submits it using the author's account for signing and submission. + * + * ### Functionality: + * - **Checks for existing schema**: Verifies if the schema is already registered on the blockchain. + * - **Encodes schema in CBOR**: Ensures schema data is serialized efficiently. + * - **Creates and signs the extrinsic**: Uses the blockchain's `create` method for schema storage. + * - **Transaction submission**: Signs and submits the extrinsic to the blockchain using the provided author's account. + * + * ### Parameters: + * @param schema - An `ISchema` object representing the structured data definition for the Cord network. + * This object defines the schema’s structure and requirements. + * @param authorAccount - A `CordKeyringPair` representing the blockchain account of the author, + * used to sign and submit the schema transaction. + * + * ### Returns: + * @returns A promise that resolves to the unique schema ID (`SchemaId`) upon successful storage. + * If the schema is already stored, it returns the existing schema's `$id`. + * + * ### Throws: + * @throws {SDKErrors.CordDispatchError} If an error occurs during the dispatch process, such as: + * - Schema creation issues. + * - Network connectivity problems. + * - Transaction signing or submission failure. + * + * ### Example Usage: + * ```typescript + * async function exampleSchemaDispatch() { + * const schema = { title: 'Example Schema', properties: { id: { type: 'string' } } }; + * const authorAccount = cord.createFromUri('//Alice'); // Example keyring pair + * + * try { + * const schemaId = await dispatchToChain(schema, authorAccount); + * console.log('Schema dispatched with ID:', schemaId); + * } catch (error) { + * console.error('Error dispatching schema:', error); + * } + * } + * + * exampleSchemaDispatch(); + * ``` + */ +export async function dispatchToChain( + schema: ISchema, + authorAccount: CordKeyringPair, +): Promise { + try { + const api = ConfigService.get('api') + + const exists = await isSchemaStored(schema) + if (exists) { + return schema.$id + } + + const encodedSchema = encodeCborSchema(schema); + const extrinsic = api.tx.schemaAccounts.create(encodedSchema); + + await Chain.signAndSubmitTx(extrinsic, authorAccount) + + return schema.$id + } catch (error) { + throw new SDKErrors.CordDispatchError( + `Error dispatching to chain: "${error}".` + ) + } +} + + +/** + * (Internal Function) - Fetches and reconstructs a schema object from the blockchain using its URI. + * This function retrieves encoded schema data from the blockchain, decodes it, and constructs a structured + * schema object. + * + * @param input - The raw input data in bytes, representing the encoded schema data on the blockchain. + * @param schemaUri - The URI (`$id`) of the schema to be fetched, used to uniquely identify + * the schema on the blockchain. + * + * @returns The reconstructed schema object based on the blockchain data, adhering to the ISchema interface. + * This object includes all the decoded properties and structure of the original schema. + * + * @throws {SDKErrors.SchemaError} Thrown when the input data cannot be decoded into a valid schema, or if the + * specified schema is not found on the blockchain. This error provides details + * about the nature of the decoding or retrieval issue. + * + * @internal + */ +function schemaInputFromChain( + input: Bytes, + schemaUri: ISchema['$id'] +): ISchema { + try { + const base64Input = input.toUtf8() + const binaryData = Buffer.from(base64Input, 'base64') + + const encoder = new Cbor.Encoder({ pack: true, useRecords: true }) + const decodedSchema = encoder.decode(binaryData) + + const reconstructedSchema: ISchema = { + $id: schemaUri, + ...decodedSchema, + } + // If throws if the input was a valid JSON but not a valid Schema. + verifyDataStructure(reconstructedSchema) + return reconstructedSchema + } catch (cause) { + throw new SDKErrors.SchemaError( + `The provided payload cannot be parsed as a Schema: ${input.toHuman()}`, + { cause } + ) + } +} + + +/** + * (Internal Function) - Converts a blockchain-encoded schema entry to a more readable and usable format. + * This helper function is crucial within the schema retrieval process, particularly in the `fetchFromChain` + * operation, where it translates schema data retrieved from the blockchain into a format suitable for + * application use. It ensures the raw, encoded data from the blockchain is transformed into a format that + * is compatible with the application's data structures. + * + * @param encodedEntry - The blockchain-encoded schema entry. It is + * wrapped in an `Option` type to handle the possibility that the schema might not exist. + * @param schemaUri - The URI (`$id`) of the schema being processed. + * + * @returns Returns an `ISchemaAccountsDetails` object containing the schema information + * if the schema exists on the blockchain. If the schema does not exist, it returns `null`. + * + * This function is vital for interpreting and converting blockchain-specific encoded schema data into + * a structured and readable format, facilitating its use within the application. + * + * @internal + */ +function fromChain( + encodedEntry: Option, + schemaUri: ISchema['$id'] +): ISchemaAccountsDetails | null { + if (encodedEntry.isSome) { + const unwrapped = encodedEntry.unwrap() + const { schema, digest, creator } = unwrapped + return { + schema: schemaInputFromChain(schema, schemaUri), + digest: digest.toHex() as SchemaDigest, + + // TODO: Check if there is any other way to do it. + // Originally it is done as Did.fromChain(creator) + creatorUri: `did:cord:3${creator}`, + } + } + return null +} + + +/** + * Retrieves schema details from the blockchain using a given schema ID. This function plays a crucial role + * in accessing stored schemas within a blockchain environment. It queries the blockchain to fetch the schema + * associated with the provided schema ID, facilitating the retrieval of schema information stored in an + * immutable and secure manner. + * + * @param schemaUri - The unique identifier of the schema, formatted as a URI string. + * This ID is used to locate and retrieve the schema on the blockchain, ensuring accuracy in schema retrieval. + * + * @returns - A promise that resolves to the schema details (`ISchemaDetails`) + * if found on the blockchain. If the schema is not present, the promise resolves to `null`. + * This approach provides a straightforward method for accessing schema information by their unique identifiers. + * + * The function employs a `try-catch` block to handle any errors during the blockchain query process. If the + * schema is not found or if an error occurs during fetching, appropriate exceptions are thrown to indicate + * the issue. + * + * @throws {SDKErrors.SchemaError} - Thrown if the schema with the provided ID is not found on the blockchain, + * providing clarity in cases where the requested data is missing. + * @throws {SDKErrors.CordFetchError} - Thrown in case of errors during the fetching process, such as network + * issues or problems with querying the blockchain. + * + * @example + * ```typescript + * async function getSchemaDetails(schemaUri: string) { + * try { + * const schemaDetails = await fetchFromChain(schemaUri); + * if (schemaDetails) { + * console.log('Fetched Schema Details:', schemaDetails); + * } else { + * console.log('Schema not found on the blockchain.'); + * } + * } catch (error) { + * console.error('Error fetching schema:', error); + * } + * } + * + * // Example usage + * getSchemaDetails('your_schema_uri'); + * ``` + */ +export async function fetchFromChain( + schemaUri: ISchema['$id'] +): Promise { + try { + const api = ConfigService.get('api') + const cordSchemaId = uriToIdentifier(schemaUri) + + const schemaEntry = await api.query.schemaAccounts.schemas(cordSchemaId) + const decodedSchema = fromChain(schemaEntry, schemaUri) + + if (decodedSchema === null) { + throw new SDKErrors.SchemaError( + `There is not a Schema with the provided URI "${schemaUri}" on chain.` + ) + } + + return decodedSchema + } catch (error) { + console.error('Error fetching schema from chain:', error) + throw new SDKErrors.CordFetchError( + `Error occurred while fetching schema from chain: ${error}` + ) + } +} diff --git a/packages/schema-accounts/src/SchemaAccounts.ts b/packages/schema-accounts/src/SchemaAccounts.ts new file mode 100644 index 00000000..8031c837 --- /dev/null +++ b/packages/schema-accounts/src/SchemaAccounts.ts @@ -0,0 +1,414 @@ +/** + * @packageDocumentation + * @module SchemaAccounts + * @preferred + * + * This module provides functionalities for defining, validating, and manipulating + * schemas within the Cord network with account based ops. It includes a set of interfaces, types, and functions + * that collectively enable the creation, verification, and management of structured + * data schemas. These schemas are used to ensure data consistency, integrity, and + * compliance with predefined formats across the network. + * + * Key Features: + * - Schema Definition: Define the structure of data using a set of predefined types + * and interfaces, including support for nested objects, arrays, and references. + * - Schema Validation: Validate data objects against defined schemas to ensure they + * meet the required structure and data types, enhancing data integrity and reliability. + * - Schema Serialization: Convert schema definitions into serialized formats for + * storage or transmission, and deserialize them back into structured objects. + * - Schema Versioning: Manage different versions of schemas, allowing for backward + * compatibility and evolution of data structures over time. + * - Nested Schema Support: Handle complex data structures with nested schemas, + * enabling the representation of intricate data models. + * + * Example: + * ``` + * import { ISchema, fromProperties } from './SchemaAccounts'; + * + * // Define a simple schema + * const userSchema = fromProperties( + * 'UserSchema', + * { + * name: { type: 'string' }, + * age: { type: 'integer' }, + * }, + * ['name', 'age'], + * 'creatorId' + * ); + * + * // Validate an object against the schema + * try { + * verifyObjectAgainstSchema({ name: 'Alice', age: 30 }, userSchema); + * console.log('Validation successful'); + * } catch (error) { + * console.error('Validation failed', error); + * } + * ``` + * + * This module is a cornerstone in ensuring that data transformation using te SDK is + * structured, reliable, and adheres to defined standards, thereby facilitating + * consistent and predictable interactions across the network. + */ + +import type { + ISchema, + ISchemaAccountsDetails, + ISchemaMetadata, + SchemaDigest, + DidUri, +} from '@cord.network/types' +import { + Crypto, + JsonSchema, + SDKErrors, + jsonabc, + Cbor, +} from '@cord.network/utils' +import { SchemaModel, MetadataModel, SchemaModelV1 } from './SchemaAccounts.types.js' +import { getUriForSchema } from './SchemaAccounts.chain.js' + + +/** + * (Internal Function) - Serializes a given schema object using CBOR encoding for consistent + * hashing, comparison, or storage. This ensures a standardized representation by ignoring + * the `$id` field (if present) and sorting the schema properties deterministically. + * + * ### Functionality: + * - **Removes `$id`**: Strips the `$id` field from the schema to ensure consistent serialization. + * - **Sorts properties**: Uses a deterministic sorting algorithm to guarantee the same encoding + * for logically identical schemas, crucial for hashing. + * - **CBOR Encoding**: Encodes the sorted schema in CBOR (Concise Binary Object Representation), + * a compact binary format suitable for storage and transmission. + * - **Base64 Conversion**: Converts the encoded schema to a Base64 string, facilitating + * storage, transmission, and hashing. + * + * ### Parameters: + * @param schema - The schema object to be serialized. It can include or exclude the `$id` field, + * as this field is ignored during serialization for consistency. + * + * ### Returns: + * @returns A Base64 string representing the serialized CBOR encoding of the schema (without the `$id` field). + * This string can be used for hashing, comparison, or storage. + * + * ### Example Usage: + * ```typescript + * const schema = { + * title: 'Example Schema', + * properties: { name: { type: 'string' }, age: { type: 'number' } }, + * $id: 'schema-id' + * }; + * + * const encodedSchema = encodeCborSchema(schema); + * console.log('Encoded CBOR Schema:', encodedSchema); + * ``` + * + * ### Internal Usage: + * This function is primarily intended for internal use, where schema objects need to be hashed + * or compared without being affected by non-functional fields like `$id`. + */ +export function encodeCborSchema( + schema: ISchema | Omit +): string { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { $id, ...schemaWithoutId } = schema as ISchema + const sortedSchema = jsonabc.sortObj(schemaWithoutId) + + const encoder = new Cbor.Encoder({ pack: true, useRecords: true }) + + const encodedSchema = encoder.encode(sortedSchema) + const cborSchema = encodedSchema.toString('base64') + + return cborSchema +} + + +/** + * (Internal Function) - Generates a hash for a given schema object. + * + * This function is used to create a unique hash value for a schema. It first serializes + * the schema object (excluding the `$id` property if present) and then generates a hash + * from this serialized string. + * + * @internal + * @param schema - The schema object to be hashed. + * This can be a full schema object (including `$id`) or any schema object without `$id`. + * @returns - The hash value of the schema as a hexadecimal string. + */ +export function getHashForSchema( + schema: ISchema | Omit +): SchemaDigest { + const encodedSchema = encodeCborSchema(schema) + return Crypto.hashStr(encodedSchema) +} + + +/** + * (Internal Function) - Validates an incoming schema object against a JSON schema model (draft-07). + * + * This function takes an object and a JSON schema, then uses a JSON Schema Validator + * to determine if the object conforms to the schema. It supports validation against + * complex schemas that may include references to other schemas. If the object does not + * conform to the schema, the function throws an error with details about the validation + * failures. + * + * @param object - The object to be validated against the schema. + * @param schema - The JSON schema to validate the object against. + * @param [messages] - An optional array to store error messages. If provided, + * validation errors will be pushed into this array. + * @param [referencedSchemas] - An optional array of additional schemas + * that might be referenced in the main schema. This is useful for complex schemas that + * include references to other schemas. + * @throws {SDKErrors.ObjectUnverifiableError} - Throws an error if the object does not + * conform to the schema. The error includes details about the validation failures. + * + * @internal + */ +export function verifyObjectAgainstSchema( + object: Record, + schema: JsonSchema.Schema, + messages?: string[], + referencedSchemas?: JsonSchema.Schema[] +): void { + const validator = new JsonSchema.Validator(schema, '7', false) + + if (referencedSchemas) { + referencedSchemas.forEach((i) => validator.addSchema(i)) + } + const { valid, errors } = validator.validate(object) + if (valid === true) return + if (messages) { + errors.forEach((error) => { + messages.push(error.error) + }) + } + throw new SDKErrors.ObjectUnverifiableError( + 'JSON schema verification failed for object', + { cause: errors } + ) +} + + +// /** +// * (Internal Function) - Validates the contents of a document against a specified schema. +// * +// * This function is designed to ensure that the contents of a document conform to a +// * predefined schema. It performs two key validations: first, it validates the schema +// * itself against a standard schema model to ensure the schema's structure is correct; +// * second, it validates the actual contents of the document against the provided schema. +// * +// * @param contents - The contents of the document to be validated. +// * This is typically a JSON object representing the data structure of the document. +// * @param schema - The schema against which the document's contents are to be validated. +// * This schema defines the expected structure, types, and constraints of the document's contents. +// * @param [messages] - An optional array to store error messages. If provided, +// * validation errors will be pushed into this array. +// * @throws {SDKErrors.ObjectUnverifiableError} - Throws an error if the schema itself is invalid +// * or if the document's contents do not conform to the schema. The error includes details +// * about the validation failures. +// * +// * @internal +// */ +// export function verifyContentAgainstSchema( +// contents: string, +// schema: ISchema, +// messages?: string[] +// ): void { +// verifyObjectAgainstSchema(schema, SchemaModel, messages) +// verifyObjectAgainstSchema(contents, schema, messages) +// } + + +/** + * (Internal Function) - Validates the structure and identifier of a schema to ensure consistency + * and correctness within the Cord network. + * + * ### Functionality: + * 1. **Schema Structure Validation**: The function checks that the provided schema conforms to the + * expected format as defined by `SchemaModel`. This ensures the schema's structure adheres to + * required standards. + * 2. **Identifier Validation**: The schema's `$id` (identifier) is verified against a URI generated + * using the schema's content, the creator's DID, and the space identifier. This ensures the uniqueness + * and correctness of the schema’s identifier within the network. + * + * ### Parameters: + * @param input - The schema object to validate. It must comply with the `ISchema` interface + * structure, including a valid `$id` property. + * @param creator - The decentralized identifier (DID) of the schema's creator. This DID + * contributes to the URI generation, ensuring traceability of the creator. + * + * ### Throws: + * @throws {SDKErrors.SchemaIdMismatchError} - If the actual `$id` of the schema does not match the + * expected URI generated using the schema's content, creator's DID, and space identifier. + * This error ensures the schema’s identifier is accurate and prevents conflicts in schema + * identification. + * + * ### Example Usage: + * ```typescript + * try { + * verifySchemaStructure(schemaObject); + * console.log('Schema is valid and consistent.'); + * } catch (error) { + * console.error('Schema validation failed:', error); + * } + * ``` + * + * ### Internal Usage: + * This function plays a critical role in maintaining data integrity and preventing inconsistencies + * in schema management by ensuring that every schema’s identifier is correctly derived from its content + * and metadata. + */ +export function verifySchemaStructure( + input: ISchema, +): void { + verifyObjectAgainstSchema(input, SchemaModel) + const uriFromSchema = getUriForSchema(input) + if (uriFromSchema.uri !== input.$id) { + throw new SDKErrors.SchemaIdMismatchError(uriFromSchema.uri, input.$id) + } +} + + +/** + * (Internal Function) - Validates the structure of a given data input against a predefined schema model. + * + * @param input - The data input to be validated. This input should be structured + * according to the ISchema interface, which defines the expected format and rules for the data. + * @throws {SDKErrors.ObjectUnverifiableError} - Throws an error if the data input does not + * conform to the schema model. This error includes details about the specific validation + * failures, aiding in diagnosing and correcting the structure of the input. + * + * @internal + */ +export function verifyDataStructure(input: ISchema): void { + verifyObjectAgainstSchema(input, SchemaModel) +} + + +/** + * (Internal Function) - Validates the metadata of a schema against a predefined metadata model. This function + * ensures that the metadata associated with a schema adheres to specific standards and + * formats as defined in the MetadataModel. + * + * @param metadata - The metadata object associated with a schema. This + * object contains various metadata fields (like title, description, etc.) that provide + * additional context and information about the schema. + * @throws {SDKErrors.ObjectUnverifiableError} - Throws an error if the metadata does not + * conform to the MetadataModel. This error includes details about the specific validation + * failures, which helps in identifying and correcting issues in the metadata structure. + * + * @internal + */ +export function verifySchemaMetadata(metadata: ISchemaMetadata): void { + verifyObjectAgainstSchema(metadata, MetadataModel) +} + + +/** + * Constructs a schema object from specified properties, assigning unique identifiers and ensuring + * compliance with schema standards. This function simplifies schema creation by generating a structured + * schema with the appropriate metadata, making it ready for use in validation, storage, or transmission. + * + * ### Functionality: + * 1. **Schema Creation and Metadata Assignment**: The input properties are used to construct the schema object, + * with additional metadata like `$schema` and `additionalProperties` flags set according to `SchemaModelV1`. + * 2. **URI and Digest Generation**: A unique URI and digest are computed for the schema content using `getUriForSchema`. + * This ensures the schema is uniquely identifiable and tamper-proof. + * 3. **DID-based Traceability**: The creator's address is converted into a DID-compliant URI (`did:cord:3
`), + * facilitating traceability. + * 4. **Schema Verification**: The constructed schema is verified for structure and consistency using `verifySchemaStructure`. + * + * ### Parameters: + * @param schema - An object defining the structure, properties, and constraints of the schema. It + * conforms to the `ISchema` interface and serves as the foundation for the final schema object. + * @param creatorAddress - The blockchain address of the schema's creator. This address is formatted into + * a DID URI, ensuring the creator's identity is associated with the schema. + * + * ### Returns: + * @returns {ISchemaAccountsDetails} - An object containing: + * - **schema**: The finalized schema object, including all properties, constraints, and a unique URI. + * - **digest**: A cryptographic digest of the schema, ensuring data integrity. + * - **creatorUri**: The creator's DID URI, enabling identity tracking. + * + * ### Throws: + * @throws {SDKErrors.SchemaStructureError} - If the constructed schema does not meet the required standards or structure, + * ensuring integrity and compliance. + * + * ### Example Usage: + * ```typescript + * const properties = { + * title: 'Person', + * type: 'object', + * properties: { + * name: { type: 'string' }, + * age: { type: 'integer' }, + * }, + * required: ['name'] + * }; + * const creatorAddress = '5F3sa2TJ...'; // Example address + * + * try { + * const { schema, digest, creatorUri } = buildFromProperties(properties, creatorAddress); + * console.log('Constructed Schema:', schema); + * console.log('Schema Digest:', digest); + * console.log('Creator URI:', creatorUri); + * } catch (error) { + * console.error('Error constructing schema:', error); + * } + * ``` + * + * ### Internal Logic: + * 1. **Setting Schema Metadata**: Ensures `additionalProperties` is false and `$schema` points to `SchemaModelV1`. + * 2. **Generating URI and Digest**: Uses `getUriForSchema` to derive the URI and digest. + * 3. **Verifying Schema**: Calls `verifySchemaStructure` to ensure the schema’s correctness. + */ +export function buildFromProperties( + schema: ISchema, + creatorAddress: string +): ISchemaAccountsDetails { + const { $id, ...uriSchema } = schema; + + uriSchema.additionalProperties = false; + uriSchema.$schema = SchemaModelV1.$id; + + const { uri, digest } = getUriForSchema(uriSchema); + + const schemaType = { + $id: uri, + ...uriSchema, + } + + const creatorUri = `did:cord:3${creatorAddress}` as DidUri; + + const schemaDetails: ISchemaAccountsDetails = { + schema: schemaType, + digest, + creatorUri, + } + + verifySchemaStructure(schemaType); + return schemaDetails +} + + +/** + * (Internal Helper Function) - Determines whether a given input conforms to the ISchema interface. This function + * serves as a type guard, verifying if the input structure aligns with the expected + * schema structure defined by ISchema. + * + * @param input - The input to be checked. This is an unknown type, which + * allows the function to be used in a variety of contexts where the type of the input + * is not predetermined. + * @returns - Returns true if the input conforms to the ISchema interface, + * indicating that it has the expected structure and properties of a schema. Returns + * false otherwise. + * + * @internal + */ +export function isISchema(input: unknown): input is ISchema { + try { + verifyDataStructure(input as ISchema) + } catch (error) { + return false + } + return true +} diff --git a/packages/schema-accounts/src/SchemaAccounts.types.ts b/packages/schema-accounts/src/SchemaAccounts.types.ts new file mode 100644 index 00000000..d957ab44 --- /dev/null +++ b/packages/schema-accounts/src/SchemaAccounts.types.ts @@ -0,0 +1,371 @@ +/** + * @packageDocumentation. + * @module SchemaAccounts/Types + * Schema Accounts Definitions Module. + * + * This module contains a collection of constants that define various JSON schemas used within the SDK. + * These schemas are fundamental in enforcing the structure and validation of data throughout the application. + * Each constant represents a specific schema with a defined structure, catering to different aspects of the + * system's needs. + * + * Included Constants: + * - `SchemaModelV1`: This is the first version of the core schema model. It defines the structure for validating + * stream types and includes a comprehensive set of properties and validation rules specific to the application's + * requirements. + * + * - `SchemaModel`: Extends the `SchemaModelV1` and includes additional validation rules and structures. It is + * designed to ensure that schemas are not only compliant with `SchemaModelV1` but also include additional + * specifications required by the broader application context. + * + * - `MetadataModel`: Defines the schema for handling metadata associated with other schemas. It provides a + * structured format for capturing metadata details such as titles, descriptions, and custom properties, + * ensuring consistency and integrity of metadata across various schemas. + * + * These constants are marked as `@internal` and are intended for use within the SDK. They are not part of + * the public API and are critical for the internal mechanisms of schema management and validation. The module + * provides a centralized and standardized approach to handling schema definitions, streamlining the process + * of schema usage and enforcement across the application. + * + * Usage of these constants should be limited to internal SDK functions and modules, as they are key to maintaining + * the integrity and consistency of data structures within the application. + */ + +import { JsonSchema } from '@cord.network/utils' + +/** + * (Internal Constant) SchemaModelV1 - Defines the JSON schema for CORD Metaschema. + * This schema is used internally for validating stream types and includes various + * properties and definitions. It specifies the structure of schema objects, including + * their types, properties, and other validation rules. This constant combines the standard + * JSON Schema structure with specific constraints and patterns relevant to CORD Metaschema. + * It is not part of the public API and is intended for internal use within the SDK. + * + * The schema includes definitions for different data types like string, number, boolean, + * array, object, and schema references. It enforces strict patterns and types for + * schema properties, ensuring the integrity and consistency of the data. + * + * @internal + * @type {JsonSchema.Schema & { $id: string }} + */ +export const SchemaModelV1: JsonSchema.Schema & { $id: string } = { + $id: 'http://cord.network/draft-01/schema#', + $schema: 'http://json-schema.org/draft-07/schema#', + title: 'CORD Metaschema', + description: 'Describes a JSON schema for validating stream types.', + type: 'object', + properties: { + $id: { + pattern: '^schema:cord:V[0-9a-zA-Z]+$', + type: 'string', + }, + $schema: { + type: 'string', + }, + $metadata: { + type: 'object', + properties: { + version: { + type: 'string', + }, + slug: { + type: 'string', + }, + discoverable: { + type: 'boolean', + }, + }, + }, + title: { type: 'string' }, + description: { + type: 'string', + }, + type: { const: 'object', type: 'string' }, + properties: { + patternProperties: { + '^.+$': { + oneOf: [ + { $ref: '#/definitions/string' }, + { $ref: '#/definitions/number' }, + { $ref: '#/definitions/boolean' }, + { $ref: '#/definitions/schemaReference' }, + { $ref: '#/definitions/array' }, + { $ref: '#/definitions/object' }, + ], + }, + }, + type: 'object', + }, + additionalProperties: { const: false, type: 'boolean' }, + required: { type: 'array', items: { type: 'string' } }, + }, + additionalProperties: false, + required: [ + '$id', + '$schema', + 'additionalProperties', + 'properties', + 'title', + 'type', + ], + definitions: { + schemaReference: { + additionalProperties: false, + properties: { + $ref: { + pattern: '^schema:cord:V[0-9a-zA-Z]+(#/properties/.+)?$', + format: 'uri', + type: 'string', + }, + }, + required: ['$ref'], + }, + string: { + additionalProperties: false, + properties: { + type: { + const: 'string', + }, + format: { enum: ['date', 'time', 'uri'] }, + enum: { + type: 'array', + items: { type: 'string' }, + }, + minLength: { + type: 'number', + }, + maxLength: { + type: 'number', + }, + }, + required: ['type'], + }, + boolean: { + additionalProperties: false, + properties: { + type: { + const: 'boolean', + }, + }, + required: ['type'], + }, + number: { + additionalProperties: false, + properties: { + type: { + enum: ['integer', 'number'], + }, + enum: { + type: 'array', + items: { type: 'number' }, + }, + minimum: { + type: 'number', + }, + maximum: { + type: 'number', + }, + }, + required: ['type'], + }, + array: { + additionalProperties: false, + properties: { + type: { const: 'array' }, + items: { + oneOf: [ + { $ref: '#/definitions/string' }, + { $ref: '#/definitions/number' }, + { $ref: '#/definitions/boolean' }, + { $ref: '#/definitions/schemaReference' }, + ], + }, + minItems: { + type: 'number', + }, + maxItems: { + type: 'number', + }, + }, + required: ['type', 'items'], + }, + object: { + additionalProperties: false, + properties: { + type: { const: 'object' }, + properties: { + type: 'object', + patternProperties: { + '^.+$': { + oneOf: [ + { $ref: '#/definitions/string' }, + { $ref: '#/definitions/number' }, + { $ref: '#/definitions/boolean' }, + { $ref: '#/definitions/schemaReference' }, + { $ref: '#/definitions/array' }, + { $ref: '#/definitions/object' }, + ], + }, + }, + }, + patternProperties: { + '^.+$': { + oneOf: [ + { $ref: '#/definitions/string' }, + { $ref: '#/definitions/number' }, + { $ref: '#/definitions/boolean' }, + { $ref: '#/definitions/schemaReference' }, + { $ref: '#/definitions/array' }, + { $ref: '#/definitions/object' }, + ], + }, + }, + }, + required: ['type'], + }, + }, +} + +/** + * (Internal Constant) SchemaModel - Represents the JSON schema model for the SDK. + * This constant is an extension of the standard JSON Schema, specifically tailored to + * integrate with the SchemaModelV1. It is used internally to validate and manage schema + * definitions within the system. Notably, it employs a combination of `allOf` and `$ref` + * to ensure compliance with the SchemaModelV1, and it defines additional structures as + * needed for the broader schema management context. + * + * The `allOf` property is used to combine multiple schema definitions, ensuring that + * any schema validated against SchemaModel must also be valid according to the SchemaModelV1. + * The `$ref` property references the SchemaModelV1 to maintain consistency and integrity + * across schema definitions. + * + * This configuration is integral to maintaining a consistent and valid schema structure + * across the SDK but is not intended for external use. + * + * @internal + * @type {JsonSchema.Schema} + */ +export const SchemaModel: JsonSchema.Schema = { + $schema: 'http://json-schema.org/draft-07/schema', + allOf: [ + { + properties: { + $schema: { + type: 'string', + const: SchemaModelV1.$id, + }, + }, + }, + { + $ref: SchemaModelV1.$id, + }, + ], + + definitions: { + [SchemaModelV1.$id]: SchemaModelV1, + }, +} + +/** + * (Internal Constant) MetadataModel - Defines the JSON schema for metadata associated with other schemas in the SDK. + * This model is specifically structured to capture metadata information such as titles, descriptions, and + * properties of schemas. It is utilized internally for managing and validating metadata in a consistent and + * structured format. The schema enforces a specific structure for metadata, ensuring all necessary fields are + * present and correctly formatted. + * + * The `properties` object within the `metadata` property provides a flexible yet structured approach to defining + * metadata. It allows for default values and pattern-based properties, ensuring broad applicability while maintaining + * strict validation rules. This model is crucial for ensuring the integrity and usability of metadata across different + * schemas in the system. + * + * This schema is an essential part of the internal mechanism of the SDK for handling schema metadata but is not + * intended for external use. It plays a key role in maintaining the consistency and standardization of schema + * metadata across the platform. + * + * @internal + * @type {JsonSchema.Schema} + */ +export const MetadataModel: JsonSchema.Schema = { + $id: 'http://cord.network/draft-01/schema-metadata', + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: { + metadata: { + type: 'object', + properties: { + title: { + type: 'object', + properties: { + default: { + type: 'string', + }, + }, + patternProperties: { + '^.*$': { + type: 'string', + }, + }, + required: ['default'], + }, + description: { + type: 'object', + properties: { + default: { + type: 'string', + }, + }, + patternProperties: { + '^.*$': { + type: 'string', + }, + }, + required: ['default'], + }, + properties: { + type: 'object', + patternProperties: { + '^.*$': { + type: 'object', + properties: { + title: { + type: 'object', + properties: { + default: { + type: 'string', + }, + }, + patternProperties: { + '^.*$': { + type: 'string', + }, + }, + required: ['default'], + }, + description: { + type: 'object', + properties: { + default: { + type: 'string', + }, + }, + patternProperties: { + '^.*$': { + type: 'string', + }, + }, + required: ['default'], + }, + }, + required: ['title'], + additionalProperties: false, + }, + }, + }, + }, + required: ['title', 'properties'], + additionalProperties: false, + }, + schemaId: { type: 'string', minLength: 1 }, + }, + required: ['metadata', 'schemaId'], + additionalProperties: false, +} diff --git a/packages/schema-accounts/src/index.ts b/packages/schema-accounts/src/index.ts new file mode 100644 index 00000000..cf7173c9 --- /dev/null +++ b/packages/schema-accounts/src/index.ts @@ -0,0 +1,3 @@ +export * from './SchemaAccounts.js' +export * from './SchemaAccounts.chain.js' +export * as TypeSchemaAccounts from './SchemaAccounts.types.js' diff --git a/packages/schema-accounts/tsconfig.build.json b/packages/schema-accounts/tsconfig.build.json new file mode 100644 index 00000000..d59aa31c --- /dev/null +++ b/packages/schema-accounts/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "module": "CommonJS", + "outDir": "./lib/cjs" + }, + + "include": [ + "src/**/*.ts", "src/**/*.js" + ], + + "exclude": [ + "coverage", + "**/*.spec.ts", + ] +} diff --git a/packages/schema-accounts/tsconfig.esm.json b/packages/schema-accounts/tsconfig.esm.json new file mode 100644 index 00000000..7f9d9b1f --- /dev/null +++ b/packages/schema-accounts/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "module": "ES6", + "outDir": "./lib/esm" + } +} \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 26b944c7..4469ebfc 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -48,6 +48,7 @@ "@cord.network/network-score": "workspace:*", "@cord.network/registries": "workspace:*", "@cord.network/schema": "workspace:*", + "@cord.network/schema-accounts": "workspace:*", "@cord.network/statement": "workspace:*", "@cord.network/types": "workspace:*", "@cord.network/utils": "workspace:*" diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 0b3fd01a..2e0469f7 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -11,4 +11,5 @@ export * as Score from '@cord.network/network-score' export * as Utils from '@cord.network/utils' export * as Registries from '@cord.network/registries' export * as Entries from '@cord.network/entries' +export * as SchemaAccounts from '@cord.network/schema-accounts' export * from '@cord.network/types' diff --git a/packages/types/src/Registries.ts b/packages/types/src/Registries.ts index 519de492..c38bb3e9 100644 --- a/packages/types/src/Registries.ts +++ b/packages/types/src/Registries.ts @@ -1,4 +1,5 @@ import type { DidUri } from './DidDocument' +import type { SchemaUri } from './Schema.js'; import { HexString } from './Imported.js' export const REGISTRY_IDENT = 9274; @@ -16,14 +17,12 @@ export interface RegistryDetails { authorizationUri: RegistryAuthorizationUri } -// TODO: Fix schemaId once schema-acc pallet becomes active -// TODO: Handle creatorUri as Did. export interface IRegistryCreate { uri: RegistryUri creatorUri: DidUri digest: RegistryDigest blob: string | null - schemaId: string | null + schemaUri: SchemaUri | null authorizationUri: RegistryAuthorizationUri } diff --git a/packages/types/src/SchemaAccounts.ts b/packages/types/src/SchemaAccounts.ts new file mode 100644 index 00000000..dbce911e --- /dev/null +++ b/packages/types/src/SchemaAccounts.ts @@ -0,0 +1,13 @@ +import type { DidUri } from './DidDocument.js' +import type { ISchema, SchemaDigest } from './Schema.js'; + +export const SCHEMA_ACCOUNTS_IDENT = 10501; + +/** + * The details of a Schema Accounts that are stored on chain. + */ +export interface ISchemaAccountsDetails { + schema: ISchema + digest: SchemaDigest + creatorUri: DidUri +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 568f16dc..592bf830 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -24,3 +24,4 @@ export * from './Keys.js' export * from './Asset.js' export * from './Registries.js' export * from './Entries.js' +export * from './SchemaAccounts.js' diff --git a/yarn.lock b/yarn.lock index 532cc2a4..7be0f6ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1745,6 +1745,7 @@ __metadata: "@cord.network/config": "workspace:*" "@cord.network/identifier": "workspace:*" "@cord.network/network": "workspace:*" + "@cord.network/schema-accounts": "workspace:*" "@cord.network/types": "workspace:*" "@cord.network/utils": "workspace:*" rimraf: "npm:^5.0.5" @@ -1752,6 +1753,24 @@ __metadata: languageName: unknown linkType: soft +"@cord.network/schema-accounts@workspace:*, @cord.network/schema-accounts@workspace:packages/schema-accounts": + version: 0.0.0-use.local + resolution: "@cord.network/schema-accounts@workspace:packages/schema-accounts" + dependencies: + "@cord.network/augment-api": "workspace:*" + "@cord.network/config": "workspace:*" + "@cord.network/did": "workspace:*" + "@cord.network/identifier": "workspace:*" + "@cord.network/network": "workspace:*" + "@cord.network/types": "workspace:*" + "@cord.network/utils": "workspace:*" + "@types/uuid": "npm:^9.0.8" + rimraf: "npm:^5.0.5" + testcontainers: "npm:^10.7.1" + typescript: "npm:^5.3.3" + languageName: unknown + linkType: soft + "@cord.network/schema@workspace:*, @cord.network/schema@workspace:packages/schema": version: 0.0.0-use.local resolution: "@cord.network/schema@workspace:packages/schema" @@ -1785,6 +1804,7 @@ __metadata: "@cord.network/network-score": "workspace:*" "@cord.network/registries": "workspace:*" "@cord.network/schema": "workspace:*" + "@cord.network/schema-accounts": "workspace:*" "@cord.network/statement": "workspace:*" "@cord.network/types": "workspace:*" "@cord.network/utils": "workspace:*"