diff --git a/.gitignore b/.gitignore index e3f4648f..6ae0ed80 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,6 @@ v8-compile-cache-0 /prep /.iroha /packages/core/crypto/wasm/ -**/*_generated_.ts +**/*generated.ts /packages/core/data-model/schema/schema.json diff --git a/deno.jsonc b/deno.jsonc index 88b562d1..b45ae76b 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -35,7 +35,7 @@ "dependencies": ["prep:ok"] }, "test:deno": { - "command": "deno test --doc", + "command": "deno test -R --doc", "dependencies": ["prep:ok"] }, "test:vitest": { @@ -74,7 +74,7 @@ }, "fmt": { "include": ["."], - "exclude": ["**/*_generated_.ts", "**/pnpm-lock.yaml"], + "exclude": ["**/*generated.ts", "**/pnpm-lock.yaml"], "semiColons": false, "lineWidth": 120, "singleQuote": true @@ -91,7 +91,9 @@ "@std/assert": "jsr:@std/assert@^1.0.11", "@std/async": "jsr:@std/async@^1.0.10", "@std/encoding": "jsr:@std/encoding@^1.0.7", + "@std/expect": "jsr:@std/expect@^1.0.13", "@std/fmt": "jsr:@std/fmt@^1.0.5", + "@std/testing": "jsr:@std/testing@^1.0.9", "@std/toml": "jsr:@std/toml@^1.0.2", "change-case": "npm:change-case@^5.4.4", "dprint-node": "npm:dprint-node@^1.0.8", @@ -108,7 +110,7 @@ }, "publish": { "exclude": [ - "!**/*_generated_.ts", + "!**/*.generated.ts", "!packages/core/crypto/wasm/", "!packages/core/data-model/schema/schema.json", "**/*.spec.ts", diff --git a/deno.lock b/deno.lock index 7e9bdd58..e13f7b49 100644 --- a/deno.lock +++ b/deno.lock @@ -5,27 +5,35 @@ "jsr:@david/path@0.2": "0.2.0", "jsr:@david/which@~0.4.1": "0.4.1", "jsr:@dprint/formatter@*": "0.4.1", + "jsr:@luca/cases@*": "1.0.0", "jsr:@std/assert@0.221": "0.221.0", + "jsr:@std/assert@^1.0.10": "1.0.11", "jsr:@std/assert@^1.0.11": "1.0.11", "jsr:@std/async@^1.0.10": "1.0.10", + "jsr:@std/async@^1.0.9": "1.0.10", "jsr:@std/bytes@0.221": "0.221.0", "jsr:@std/cli@*": "1.0.12", "jsr:@std/collections@^1.0.9": "1.0.10", "jsr:@std/crypto@*": "1.0.4", + "jsr:@std/data-structures@^1.0.6": "1.0.6", "jsr:@std/encoding@*": "1.0.7", "jsr:@std/encoding@^1.0.7": "1.0.7", "jsr:@std/expect@*": "1.0.13", + "jsr:@std/expect@^1.0.13": "1.0.13", "jsr:@std/fmt@*": "1.0.5", "jsr:@std/fmt@1": "1.0.5", "jsr:@std/fmt@^1.0.5": "1.0.5", "jsr:@std/fs@*": "1.0.11", "jsr:@std/fs@1": "1.0.11", + "jsr:@std/fs@^1.0.9": "1.0.11", "jsr:@std/internal@^1.0.5": "1.0.5", "jsr:@std/io@0.221": "0.221.0", "jsr:@std/path@*": "1.0.8", "jsr:@std/path@1": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/streams@0.221": "0.221.0", + "jsr:@std/testing@*": "1.0.9", + "jsr:@std/testing@^1.0.9": "1.0.9", "jsr:@std/text@*": "1.0.10", "jsr:@std/toml@*": "1.0.2", "jsr:@std/toml@^1.0.2": "1.0.2", @@ -95,6 +103,9 @@ "@dprint/formatter@0.4.1": { "integrity": "96449ab83aa9f72df98caa5030d3f4a921c5c29d9b0e0d0da83d79e2024a9637" }, + "@luca/cases@1.0.0": { + "integrity": "b5f9471f1830595e63a2b7d62821ac822a19e16899e6584799be63f17a1fbc30" + }, "@std/assert@0.221.0": { "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a" }, @@ -119,6 +130,9 @@ "@std/crypto@1.0.4": { "integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340" }, + "@std/data-structures@1.0.6": { + "integrity": "76a7fd8080c66604c0496220a791860492ab21a04a63a969c0b9a0609bbbb760" + }, "@std/encoding@1.0.7": { "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" }, @@ -157,6 +171,17 @@ "jsr:@std/io" ] }, + "@std/testing@1.0.9": { + "integrity": "9bdd4ac07cb13e7594ac30e90f6ceef7254ac83a9aeaa089be0008f33aab5cd4", + "dependencies": [ + "jsr:@std/assert@^1.0.10", + "jsr:@std/async@^1.0.9", + "jsr:@std/data-structures", + "jsr:@std/fs@^1.0.9", + "jsr:@std/internal", + "jsr:@std/path@^1.0.8" + ] + }, "@std/text@1.0.10": { "integrity": "9dcab377450253c0efa9a9a0c731040bfd4e1c03f8303b5934381467b7954338" }, @@ -2959,7 +2984,9 @@ "jsr:@std/assert@^1.0.11", "jsr:@std/async@^1.0.10", "jsr:@std/encoding@^1.0.7", + "jsr:@std/expect@^1.0.13", "jsr:@std/fmt@^1.0.5", + "jsr:@std/testing@^1.0.9", "jsr:@std/toml@^1.0.2", "npm:@deno/vite-plugin@^1.0.3", "npm:change-case@^5.4.4", diff --git a/etc/__snapshots__/codegen_test.ts.snap b/etc/__snapshots__/codegen_test.ts.snap new file mode 100644 index 00000000..31b6db00 --- /dev/null +++ b/etc/__snapshots__/codegen_test.ts.snap @@ -0,0 +1,695 @@ +export const snapshot = {}; + +snapshot[`generate prototypes > prototypes snapshot 1`] = ` +"import type * as lib from 'prelude' + +export type QueryCompatibleSelectors = { + FindDomains: 'domain' | 'domain-id' | 'domain-id-name' | 'domain-metadata' | 'domain-metadata-key' + FindAccounts: 'account' | 'account-id' | 'account-id-domain' | 'account-id-domain-name' | 'account-id-signatory' | 'account-metadata' | 'account-metadata-key' + FindAssets: 'asset' | 'asset-id' | 'asset-id-account' | 'asset-id-account-domain' | 'asset-id-account-domain-name' | 'asset-id-account-signatory' | 'asset-id-definition' | 'asset-id-definition-domain' | 'asset-id-definition-domain-name' | 'asset-id-definition-name' | 'asset-value' | 'asset-value-numeric' | 'asset-value-store' | 'asset-value-store-key' + FindAssetsDefinitions: 'asset-definition' | 'asset-definition-id' | 'asset-definition-id-domain' | 'asset-definition-id-domain-name' | 'asset-definition-id-name' | 'asset-definition-metadata' | 'asset-definition-metadata-key' + FindRoles: 'role' | 'role-id' | 'role-id-name' + FindRoleIds: 'role-id' | 'role-id-name' + FindPermissionsByAccountId: 'permission' + FindRolesByAccountId: 'role-id' | 'role-id-name' + FindAccountsWithAsset: 'account' | 'account-id' | 'account-id-domain' | 'account-id-domain-name' | 'account-id-signatory' | 'account-metadata' | 'account-metadata-key' + FindPeers: 'peer-id' | 'peer-id-public-key' + FindActiveTriggerIds: 'trigger-id' | 'trigger-id-name' + FindTriggers: 'trigger' | 'trigger-id' | 'trigger-id-name' | 'trigger-action' | 'trigger-action-metadata' | 'trigger-action-metadata-key' + FindTransactions: 'committed-transaction' | 'committed-transaction-block-hash' | 'committed-transaction-value' | 'committed-transaction-value-hash' | 'committed-transaction-value-authority' | 'committed-transaction-value-authority-domain' | 'committed-transaction-value-authority-domain-name' | 'committed-transaction-value-authority-signatory' | 'committed-transaction-error' + FindBlocks: 'signed-block' | 'signed-block-header' | 'signed-block-header-hash' + FindBlockHeaders: 'block-header' | 'block-header-hash' +} + +export type SelectorIdToOutput = { + 'domain': lib.Domain + 'domain-id': lib.DomainId + 'domain-id-name': lib.Name + 'domain-metadata': lib.Metadata + 'domain-metadata-key': lib.Json + 'account': lib.Account + 'account-id': lib.AccountId + 'account-id-domain': lib.DomainId + 'account-id-domain-name': lib.Name + 'account-id-signatory': lib.PublicKey + 'account-metadata': lib.Metadata + 'account-metadata-key': lib.Json + 'asset': lib.Asset + 'asset-id': lib.AssetId + 'asset-id-account': lib.AccountId + 'asset-id-account-domain': lib.DomainId + 'asset-id-account-domain-name': lib.Name + 'asset-id-account-signatory': lib.PublicKey + 'asset-id-definition': lib.AssetDefinitionId + 'asset-id-definition-domain': lib.DomainId + 'asset-id-definition-domain-name': lib.Name + 'asset-id-definition-name': lib.Name + 'asset-value': lib.AssetValue + 'asset-value-numeric': lib.Numeric + 'asset-value-store': lib.Metadata + 'asset-value-store-key': lib.Json + 'asset-definition': lib.AssetDefinition + 'asset-definition-id': lib.AssetDefinitionId + 'asset-definition-id-domain': lib.DomainId + 'asset-definition-id-domain-name': lib.Name + 'asset-definition-id-name': lib.Name + 'asset-definition-metadata': lib.Metadata + 'asset-definition-metadata-key': lib.Json + 'role': lib.Role + 'role-id': lib.RoleId + 'role-id-name': lib.Name + 'permission': lib.Permission + 'peer-id': lib.PeerId + 'peer-id-public-key': lib.PublicKey + 'trigger-id': lib.TriggerId + 'trigger-id-name': lib.Name + 'trigger': lib.Trigger + 'trigger-action': lib.Action + 'trigger-action-metadata': lib.Metadata + 'trigger-action-metadata-key': lib.Json + 'committed-transaction': lib.CommittedTransaction + 'committed-transaction-block-hash': lib.Hash + 'committed-transaction-value': lib.SignedTransaction + 'committed-transaction-value-hash': lib.Hash + 'committed-transaction-value-authority': lib.AccountId + 'committed-transaction-value-authority-domain': lib.DomainId + 'committed-transaction-value-authority-domain-name': lib.Name + 'committed-transaction-value-authority-signatory': lib.PublicKey + 'committed-transaction-error': lib.Option + 'signed-block': lib.SignedBlock + 'signed-block-header': lib.BlockHeader + 'signed-block-header-hash': lib.Hash + 'block-header': lib.BlockHeader + 'block-header-hash': lib.Hash +} + +export type QuerySelectors = { + FindDomains: { + __selector: 'domain', + id: { + __selector: 'domain-id', + name: { + __selector: 'domain-id-name', + } + } + metadata: { + __selector: 'domain-metadata', + key(key: lib.Name): { + __selector: 'domain-metadata-key', + } + } + } + FindAccounts: { + __selector: 'account', + id: { + __selector: 'account-id', + domain: { + __selector: 'account-id-domain', + name: { + __selector: 'account-id-domain-name', + } + } + signatory: { + __selector: 'account-id-signatory', + } + } + metadata: { + __selector: 'account-metadata', + key(key: lib.Name): { + __selector: 'account-metadata-key', + } + } + } + FindAssets: { + __selector: 'asset', + id: { + __selector: 'asset-id', + account: { + __selector: 'asset-id-account', + domain: { + __selector: 'asset-id-account-domain', + name: { + __selector: 'asset-id-account-domain-name', + } + } + signatory: { + __selector: 'asset-id-account-signatory', + } + } + definition: { + __selector: 'asset-id-definition', + domain: { + __selector: 'asset-id-definition-domain', + name: { + __selector: 'asset-id-definition-domain-name', + } + } + name: { + __selector: 'asset-id-definition-name', + } + } + } + value: { + __selector: 'asset-value', + numeric: { + __selector: 'asset-value-numeric', + } + store: { + __selector: 'asset-value-store', + key(key: lib.Name): { + __selector: 'asset-value-store-key', + } + } + } + } + FindAssetsDefinitions: { + __selector: 'asset-definition', + id: { + __selector: 'asset-definition-id', + domain: { + __selector: 'asset-definition-id-domain', + name: { + __selector: 'asset-definition-id-domain-name', + } + } + name: { + __selector: 'asset-definition-id-name', + } + } + metadata: { + __selector: 'asset-definition-metadata', + key(key: lib.Name): { + __selector: 'asset-definition-metadata-key', + } + } + } + FindRoles: { + __selector: 'role', + id: { + __selector: 'role-id', + name: { + __selector: 'role-id-name', + } + } + } + FindRoleIds: { + __selector: 'role-id', + name: { + __selector: 'role-id-name', + } + } + FindPermissionsByAccountId: { + __selector: 'permission', + } + FindRolesByAccountId: { + __selector: 'role-id', + name: { + __selector: 'role-id-name', + } + } + FindAccountsWithAsset: { + __selector: 'account', + id: { + __selector: 'account-id', + domain: { + __selector: 'account-id-domain', + name: { + __selector: 'account-id-domain-name', + } + } + signatory: { + __selector: 'account-id-signatory', + } + } + metadata: { + __selector: 'account-metadata', + key(key: lib.Name): { + __selector: 'account-metadata-key', + } + } + } + FindPeers: { + __selector: 'peer-id', + publicKey: { + __selector: 'peer-id-public-key', + } + } + FindActiveTriggerIds: { + __selector: 'trigger-id', + name: { + __selector: 'trigger-id-name', + } + } + FindTriggers: { + __selector: 'trigger', + id: { + __selector: 'trigger-id', + name: { + __selector: 'trigger-id-name', + } + } + action: { + __selector: 'trigger-action', + metadata: { + __selector: 'trigger-action-metadata', + key(key: lib.Name): { + __selector: 'trigger-action-metadata-key', + } + } + } + } + FindTransactions: { + __selector: 'committed-transaction', + blockHash: { + __selector: 'committed-transaction-block-hash', + } + value: { + __selector: 'committed-transaction-value', + hash: { + __selector: 'committed-transaction-value-hash', + } + authority: { + __selector: 'committed-transaction-value-authority', + domain: { + __selector: 'committed-transaction-value-authority-domain', + name: { + __selector: 'committed-transaction-value-authority-domain-name', + } + } + signatory: { + __selector: 'committed-transaction-value-authority-signatory', + } + } + } + error: { + __selector: 'committed-transaction-error', + } + } + FindBlocks: { + __selector: 'signed-block', + header: { + __selector: 'signed-block-header', + hash: { + __selector: 'signed-block-header-hash', + } + } + } + FindBlockHeaders: { + __selector: 'block-header', + hash: { + __selector: 'block-header-hash', + } + } +} + +export type QueryPredicates = { + FindDomains: { + id: { + equals: (value: lib.DomainId) => lib.DomainProjectionPredicate + name: { + equals: (value: lib.String) => lib.DomainProjectionPredicate + contains: (value: lib.String) => lib.DomainProjectionPredicate + startsWith: (value: lib.String) => lib.DomainProjectionPredicate + endsWith: (value: lib.String) => lib.DomainProjectionPredicate + } + } + metadata: { + key: (key: lib.Name) => { + equals: (value: lib.Json) => lib.DomainProjectionPredicate + } + } + } + FindAccounts: { + id: { + equals: (value: lib.AccountId) => lib.AccountProjectionPredicate + domain: { + equals: (value: lib.DomainId) => lib.AccountProjectionPredicate + name: { + equals: (value: lib.String) => lib.AccountProjectionPredicate + contains: (value: lib.String) => lib.AccountProjectionPredicate + startsWith: (value: lib.String) => lib.AccountProjectionPredicate + endsWith: (value: lib.String) => lib.AccountProjectionPredicate + } + } + signatory: { + equals: (value: lib.PublicKey) => lib.AccountProjectionPredicate + } + } + metadata: { + key: (key: lib.Name) => { + equals: (value: lib.Json) => lib.AccountProjectionPredicate + } + } + } + FindAssets: { + id: { + equals: (value: lib.AssetId) => lib.AssetProjectionPredicate + account: { + equals: (value: lib.AccountId) => lib.AssetProjectionPredicate + domain: { + equals: (value: lib.DomainId) => lib.AssetProjectionPredicate + name: { + equals: (value: lib.String) => lib.AssetProjectionPredicate + contains: (value: lib.String) => lib.AssetProjectionPredicate + startsWith: (value: lib.String) => lib.AssetProjectionPredicate + endsWith: (value: lib.String) => lib.AssetProjectionPredicate + } + } + signatory: { + equals: (value: lib.PublicKey) => lib.AssetProjectionPredicate + } + } + definition: { + equals: (value: lib.AssetDefinitionId) => lib.AssetProjectionPredicate + domain: { + equals: (value: lib.DomainId) => lib.AssetProjectionPredicate + name: { + equals: (value: lib.String) => lib.AssetProjectionPredicate + contains: (value: lib.String) => lib.AssetProjectionPredicate + startsWith: (value: lib.String) => lib.AssetProjectionPredicate + endsWith: (value: lib.String) => lib.AssetProjectionPredicate + } + } + name: { + equals: (value: lib.String) => lib.AssetProjectionPredicate + contains: (value: lib.String) => lib.AssetProjectionPredicate + startsWith: (value: lib.String) => lib.AssetProjectionPredicate + endsWith: (value: lib.String) => lib.AssetProjectionPredicate + } + } + } + value: { + isNumeric: () => lib.AssetProjectionPredicate + isStore: () => lib.AssetProjectionPredicate + numeric: never + store: { + key: (key: lib.Name) => { + equals: (value: lib.Json) => lib.AssetProjectionPredicate + } + } + } + } + FindAssetsDefinitions: { + id: { + equals: (value: lib.AssetDefinitionId) => lib.AssetDefinitionProjectionPredicate + domain: { + equals: (value: lib.DomainId) => lib.AssetDefinitionProjectionPredicate + name: { + equals: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + contains: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + startsWith: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + endsWith: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + } + } + name: { + equals: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + contains: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + startsWith: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + endsWith: (value: lib.String) => lib.AssetDefinitionProjectionPredicate + } + } + metadata: { + key: (key: lib.Name) => { + equals: (value: lib.Json) => lib.AssetDefinitionProjectionPredicate + } + } + } + FindRoles: { + id: { + equals: (value: lib.RoleId) => lib.RoleProjectionPredicate + name: { + equals: (value: lib.String) => lib.RoleProjectionPredicate + contains: (value: lib.String) => lib.RoleProjectionPredicate + startsWith: (value: lib.String) => lib.RoleProjectionPredicate + endsWith: (value: lib.String) => lib.RoleProjectionPredicate + } + } + } + FindRoleIds: { + equals: (value: lib.RoleId) => lib.RoleIdProjectionPredicate + name: { + equals: (value: lib.String) => lib.RoleIdProjectionPredicate + contains: (value: lib.String) => lib.RoleIdProjectionPredicate + startsWith: (value: lib.String) => lib.RoleIdProjectionPredicate + endsWith: (value: lib.String) => lib.RoleIdProjectionPredicate + } + } + FindPermissionsByAccountId: never + FindRolesByAccountId: { + equals: (value: lib.RoleId) => lib.RoleIdProjectionPredicate + name: { + equals: (value: lib.String) => lib.RoleIdProjectionPredicate + contains: (value: lib.String) => lib.RoleIdProjectionPredicate + startsWith: (value: lib.String) => lib.RoleIdProjectionPredicate + endsWith: (value: lib.String) => lib.RoleIdProjectionPredicate + } + } + FindAccountsWithAsset: { + id: { + equals: (value: lib.AccountId) => lib.AccountProjectionPredicate + domain: { + equals: (value: lib.DomainId) => lib.AccountProjectionPredicate + name: { + equals: (value: lib.String) => lib.AccountProjectionPredicate + contains: (value: lib.String) => lib.AccountProjectionPredicate + startsWith: (value: lib.String) => lib.AccountProjectionPredicate + endsWith: (value: lib.String) => lib.AccountProjectionPredicate + } + } + signatory: { + equals: (value: lib.PublicKey) => lib.AccountProjectionPredicate + } + } + metadata: { + key: (key: lib.Name) => { + equals: (value: lib.Json) => lib.AccountProjectionPredicate + } + } + } + FindPeers: { + publicKey: { + equals: (value: lib.PublicKey) => lib.PeerIdProjectionPredicate + } + } + FindActiveTriggerIds: { + equals: (value: lib.TriggerId) => lib.TriggerIdProjectionPredicate + name: { + equals: (value: lib.String) => lib.TriggerIdProjectionPredicate + contains: (value: lib.String) => lib.TriggerIdProjectionPredicate + startsWith: (value: lib.String) => lib.TriggerIdProjectionPredicate + endsWith: (value: lib.String) => lib.TriggerIdProjectionPredicate + } + } + FindTriggers: { + id: { + equals: (value: lib.TriggerId) => lib.TriggerProjectionPredicate + name: { + equals: (value: lib.String) => lib.TriggerProjectionPredicate + contains: (value: lib.String) => lib.TriggerProjectionPredicate + startsWith: (value: lib.String) => lib.TriggerProjectionPredicate + endsWith: (value: lib.String) => lib.TriggerProjectionPredicate + } + } + action: { + metadata: { + key: (key: lib.Name) => { + equals: (value: lib.Json) => lib.TriggerProjectionPredicate + } + } + } + } + FindTransactions: { + blockHash: { + equals: (value: lib.Hash) => lib.CommittedTransactionProjectionPredicate + } + value: { + hash: { + equals: (value: lib.Hash) => lib.CommittedTransactionProjectionPredicate + } + authority: { + equals: (value: lib.AccountId) => lib.CommittedTransactionProjectionPredicate + domain: { + equals: (value: lib.DomainId) => lib.CommittedTransactionProjectionPredicate + name: { + equals: (value: lib.String) => lib.CommittedTransactionProjectionPredicate + contains: (value: lib.String) => lib.CommittedTransactionProjectionPredicate + startsWith: (value: lib.String) => lib.CommittedTransactionProjectionPredicate + endsWith: (value: lib.String) => lib.CommittedTransactionProjectionPredicate + } + } + signatory: { + equals: (value: lib.PublicKey) => lib.CommittedTransactionProjectionPredicate + } + } + } + error: { + isSome: () => lib.CommittedTransactionProjectionPredicate + } + } + FindBlocks: { + header: { + hash: { + equals: (value: lib.Hash) => lib.SignedBlockProjectionPredicate + } + } + } + FindBlockHeaders: { + hash: { + equals: (value: lib.Hash) => lib.BlockHeaderProjectionPredicate + } + } +}" +`; + +snapshot[`generate prototypes > find api snapshot 1`] = ` +"import * as client from 'prelude' +import type * as core from '@iroha/core' +import type * as types from '@iroha/core/data-model' +export class FindAPI { + private _executor: client.QueryExecutor + public constructor(executor: client.QueryExecutor) { this._executor = executor; } + /** Convenience method for \`FindDomains\` query, a variant of {@linkcode types.QueryBox} enum. */ + public domains(params?: core.QueryBuilderParams): client.QueryBuilder<'FindDomains'> { + return new client.QueryBuilder(this._executor, 'FindDomains', params) + } + + /** Convenience method for \`FindAccounts\` query, a variant of {@linkcode types.QueryBox} enum. */ + public accounts(params?: core.QueryBuilderParams): client.QueryBuilder<'FindAccounts'> { + return new client.QueryBuilder(this._executor, 'FindAccounts', params) + } + + /** Convenience method for \`FindAssets\` query, a variant of {@linkcode types.QueryBox} enum. */ + public assets(params?: core.QueryBuilderParams): client.QueryBuilder<'FindAssets'> { + return new client.QueryBuilder(this._executor, 'FindAssets', params) + } + + /** Convenience method for \`FindAssetsDefinitions\` query, a variant of {@linkcode types.QueryBox} enum. */ + public assetsDefinitions(params?: core.QueryBuilderParams): client.QueryBuilder<'FindAssetsDefinitions'> { + return new client.QueryBuilder(this._executor, 'FindAssetsDefinitions', params) + } + + /** Convenience method for \`FindRoles\` query, a variant of {@linkcode types.QueryBox} enum. */ + public roles(params?: core.QueryBuilderParams): client.QueryBuilder<'FindRoles'> { + return new client.QueryBuilder(this._executor, 'FindRoles', params) + } + + /** Convenience method for \`FindRoleIds\` query, a variant of {@linkcode types.QueryBox} enum. */ + public roleIds(params?: core.QueryBuilderParams): client.QueryBuilder<'FindRoleIds'> { + return new client.QueryBuilder(this._executor, 'FindRoleIds', params) + } + + /** Convenience method for \`FindPermissionsByAccountId\` query, a variant of {@linkcode types.QueryBox} enum. */ + public permissionsByAccountId(payload: types.FindPermissionsByAccountId, params?: core.QueryBuilderParams): client.QueryBuilder<'FindPermissionsByAccountId'> { + return new client.QueryBuilder(this._executor, 'FindPermissionsByAccountId', payload, params) + } + + /** Convenience method for \`FindRolesByAccountId\` query, a variant of {@linkcode types.QueryBox} enum. */ + public rolesByAccountId(payload: types.FindRolesByAccountId, params?: core.QueryBuilderParams): client.QueryBuilder<'FindRolesByAccountId'> { + return new client.QueryBuilder(this._executor, 'FindRolesByAccountId', payload, params) + } + + /** Convenience method for \`FindAccountsWithAsset\` query, a variant of {@linkcode types.QueryBox} enum. */ + public accountsWithAsset(payload: types.FindAccountsWithAsset, params?: core.QueryBuilderParams): client.QueryBuilder<'FindAccountsWithAsset'> { + return new client.QueryBuilder(this._executor, 'FindAccountsWithAsset', payload, params) + } + + /** Convenience method for \`FindPeers\` query, a variant of {@linkcode types.QueryBox} enum. */ + public peers(params?: core.QueryBuilderParams): client.QueryBuilder<'FindPeers'> { + return new client.QueryBuilder(this._executor, 'FindPeers', params) + } + + /** Convenience method for \`FindActiveTriggerIds\` query, a variant of {@linkcode types.QueryBox} enum. */ + public activeTriggerIds(params?: core.QueryBuilderParams): client.QueryBuilder<'FindActiveTriggerIds'> { + return new client.QueryBuilder(this._executor, 'FindActiveTriggerIds', params) + } + + /** Convenience method for \`FindTriggers\` query, a variant of {@linkcode types.QueryBox} enum. */ + public triggers(params?: core.QueryBuilderParams): client.QueryBuilder<'FindTriggers'> { + return new client.QueryBuilder(this._executor, 'FindTriggers', params) + } + + /** Convenience method for \`FindTransactions\` query, a variant of {@linkcode types.QueryBox} enum. */ + public transactions(params?: core.QueryBuilderParams): client.QueryBuilder<'FindTransactions'> { + return new client.QueryBuilder(this._executor, 'FindTransactions', params) + } + + /** Convenience method for \`FindBlocks\` query, a variant of {@linkcode types.QueryBox} enum. */ + public blocks(params?: core.QueryBuilderParams): client.QueryBuilder<'FindBlocks'> { + return new client.QueryBuilder(this._executor, 'FindBlocks', params) + } + + /** Convenience method for \`FindBlockHeaders\` query, a variant of {@linkcode types.QueryBox} enum. */ + public blockHeaders(params?: core.QueryBuilderParams): client.QueryBuilder<'FindBlockHeaders'> { + return new client.QueryBuilder(this._executor, 'FindBlockHeaders', params) + } + + /** Convenience method for \`FindExecutorDataModel\` query, a variant of {@linkcode types.SingularQueryBox} enum. */ + public executorDataModel(): Promise> { + return client.executeSingularQuery(this._executor, 'FindExecutorDataModel') + } + + /** Convenience method for \`FindParameters\` query, a variant of {@linkcode types.SingularQueryBox} enum. */ + public parameters(): Promise> { + return client.executeSingularQuery(this._executor, 'FindParameters') + } + +}" +`; + +snapshot[`enum shortcuts > build shortcuts tree 1`] = ` +[ + { + name: "Unit", + t: "unit", + }, + { + name: "WithType", + t: "value", + value_ty: { + id: "Whichever", + t: "local", + }, + }, + { + name: "Nested", + t: "enum", + tree: { + id: "B", + variants: [ + { + name: "Bunit", + t: "unit", + }, + { + name: "Bnested", + t: "enum", + tree: { + id: "C", + variants: [ + { + name: "CUnit", + t: "unit", + }, + { + name: "Cfinal", + t: "value", + value_ty: { + id: "Whichever", + t: "local", + }, + }, + ], + }, + }, + ], + }, + }, +] +`; + +snapshot[`enum shortcuts > generate shortcut tree 1`] = ` +" + type test = { Unit: lib.VariantUnit<'Unit'>, WithType: (value: T) => lib.Variant<'WithType', T>, Nested: { Bunit: lib.Variant<'Nested', lib.VariantUnit<'Bunit'>>, Bnested: { CUnit: lib.Variant<'Nested', lib.Variant<'Bnested', lib.VariantUnit<'CUnit'>>>, Cfinal: (value: T) => lib.Variant<'Nested', lib.Variant<'Bnested', lib.Variant<'Cfinal', T>>> } } } + const test = { Unit: Object.freeze({ kind: 'Unit' }), WithType: (value: T): lib.Variant<'WithType', T> => ({ kind: 'WithType', value }), Nested: { Bunit: Object.freeze>>({ kind: 'Nested', value: B.Bunit }), Bnested: { CUnit: Object.freeze>>>({ kind: 'Nested', value: B.Bnested.CUnit }), Cfinal: (value: T): lib.Variant<'Nested', lib.Variant<'Bnested', lib.Variant<'Cfinal', T>>> => ({ kind: 'Nested', value: B.Bnested.Cfinal(value) }) } } } + " +`; diff --git a/etc/codegen.spec.ts b/etc/codegen.spec.ts deleted file mode 100644 index 0c74d17d..00000000 --- a/etc/codegen.spec.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { - type EmitCode, - type EmitsMap, - enumShortcuts, - type EnumShortcutTreeVariant, - renderShortcutsTree, -} from './codegen.ts' -import * as dprint from 'dprint-node' - -async function formatTS(code: string): Promise { - return dprint.format('whichever.ts', code, { semiColons: 'asi', quoteStyle: 'preferSingle' }) -} - -describe('enum shortcuts', () => { - const SAMPLE = { - A: { - t: 'enum', - variants: [ - { tag: 'Unit', discriminant: 0, type: { t: 'null' } }, - { tag: 'WithType', type: { t: 'local', id: 'Whichever' }, discriminant: 1 }, - { tag: 'Nested', type: { t: 'local', id: 'B' }, discriminant: 2 }, - ], - }, - B: { - t: 'enum', - variants: [ - { tag: 'Bunit', discriminant: 0, type: { t: 'null' } }, - { discriminant: 1, tag: 'Bnested', type: { t: 'local', id: 'C' } }, - ], - }, - C: { - t: 'enum', - variants: [ - { tag: 'CUnit', discriminant: 0, type: { t: 'null' } }, - { discriminant: 1, tag: 'Cfinal', type: { t: 'local', id: 'Whichever' } }, - ], - }, - Whichever: { t: 'alias', to: { t: 'lib', id: 'AccountId' } }, - } satisfies Record - - const SAMPLE_MAP = new Map(Object.entries(SAMPLE)) - - test('build shortcuts tree', () => { - const tree = enumShortcuts(SAMPLE.A.variants, SAMPLE_MAP) - - expect(tree).toMatchInlineSnapshot(` - [ - { - "name": "Unit", - "t": "unit", - }, - { - "name": "WithType", - "t": "value", - "value_ty": { - "id": "Whichever", - "t": "local", - }, - }, - { - "name": "Nested", - "t": "enum", - "tree": { - "id": "B", - "variants": [ - { - "name": "Bunit", - "t": "unit", - }, - { - "name": "Bnested", - "t": "enum", - "tree": { - "id": "C", - "variants": [ - { - "name": "CUnit", - "t": "unit", - }, - { - "name": "Cfinal", - "t": "value", - "value_ty": { - "id": "Whichever", - "t": "local", - }, - }, - ], - }, - }, - ], - }, - }, - ] - `) - }) - - test('build: takes aliases into consideration', () => { - const map: EmitsMap = new Map([ - ['Foo', { t: 'alias', to: { t: 'local', id: 'Bar' } }], - ['Bar', { t: 'enum', variants: [{ discriminant: 0, tag: 'Baz', type: { t: 'null' } }] }], - ]) - - const tree = enumShortcuts([{ discriminant: 0, tag: 'Bar', type: { t: 'local', id: 'Foo' } }], map) - - expect(tree).toMatchInlineSnapshot(` - [ - { - "name": "Bar", - "t": "enum", - "tree": { - "id": "Foo", - "variants": [ - { - "name": "Baz", - "t": "unit", - }, - ], - }, - }, - ] - `) - }) - - test('build: omits enums without variants', () => { - const map: EmitsMap = new Map([ - ['Foo', { t: 'enum', variants: [] }], - [ - 'Bar', - { - t: 'enum', - variants: [ - { discriminant: 0, tag: 'Baz', type: { t: 'null' } }, - { discriminant: 1, tag: 'Foo', type: { t: 'local', id: 'Foo' } }, - ], - }, - ], - // nested void - ['Void0', { t: 'enum', variants: [{ discriminant: 0, tag: 'Void1', type: { t: 'local', id: 'Void1' } }] }], - ['Void1', { t: 'enum', variants: [{ discriminant: 0, tag: 'Foo', type: { t: 'local', id: 'Foo' } }] }], - ]) - - const tree = enumShortcuts( - [ - { discriminant: 0, tag: 'foo', type: { t: 'local', id: 'Foo' } }, - { discriminant: 1, tag: 'bar', type: { t: 'local', id: 'Bar' } }, - { discriminant: 2, tag: 'void', type: { t: 'local', id: 'Void0' } }, - ], - map, - ) - - expect(tree).toEqual( - [ - { - name: 'bar', - t: 'enum', - tree: { - id: 'Bar', - variants: [{ name: 'Baz', t: 'unit' }], - }, - }, - ] satisfies EnumShortcutTreeVariant[], - ) - }) - - test('generate shortcut tree', async () => { - const TREE = { id: 'A', variants: enumShortcuts(SAMPLE.A.variants, SAMPLE_MAP) } - const type = renderShortcutsTree(TREE, 'type') - const value = renderShortcutsTree(TREE, 'value') - - const full = ` - type test = ${type} - const test = ${value} - ` - - expect(await formatTS(full)).toMatchInlineSnapshot( - ` - "type test = { - Unit: lib.VariantUnit<'Unit'> - WithType: (value: T) => lib.Variant<'WithType', T> - Nested: { - Bunit: lib.Variant<'Nested', lib.VariantUnit<'Bunit'>> - Bnested: { - CUnit: lib.Variant< - 'Nested', - lib.Variant<'Bnested', lib.VariantUnit<'CUnit'>> - > - Cfinal: ( - value: T, - ) => lib.Variant< - 'Nested', - lib.Variant<'Bnested', lib.Variant<'Cfinal', T>> - > - } - } - } - const test = { - Unit: Object.freeze({ kind: 'Unit' }), - WithType: ( - value: T, - ): lib.Variant<'WithType', T> => ({ kind: 'WithType', value }), - Nested: { - Bunit: Object.freeze>>({ - kind: 'Nested', - value: B.Bunit, - }), - Bnested: { - CUnit: Object.freeze< - lib.Variant<'Nested', lib.Variant<'Bnested', lib.VariantUnit<'CUnit'>>> - >({ kind: 'Nested', value: B.Bnested.CUnit }), - Cfinal: ( - value: T, - ): lib.Variant< - 'Nested', - lib.Variant<'Bnested', lib.Variant<'Cfinal', T>> - > => ({ kind: 'Nested', value: B.Bnested.Cfinal(value) }), - }, - }, - } - " - `, - ) - }) -}) diff --git a/etc/codegen.ts b/etc/codegen.ts index ac0d3970..823753fd 100644 --- a/etc/codegen.ts +++ b/etc/codegen.ts @@ -1,7 +1,7 @@ import type { EnumDefinition, NamedStructDefinition, Schema, SchemaTypeDefinition } from '@iroha/core/data-model/schema' -import { toCamelCase as camelCase } from 'jsr:@std/text' +import { toCamelCase as camelCase, toKebabCase } from 'jsr:@std/text' import { deepEqual } from 'npm:fast-equals' -import { assert as assert, fail } from '@std/assert' +import { assert, assertEquals, assertObjectMatch, fail } from '@std/assert' import { match, P } from 'ts-pattern' // TODO: return HashOf<..> ? Hard. Requires all hashable items to implement Hash on instances => why not all make all types as classes, finally... @@ -14,7 +14,6 @@ export function generateDataModel(resolver: Resolver, libModule: string): string return [ `import * as lib from '${libModule}'`, ...arranged.map((id) => renderEmit(id, emits)), - ...generateQueryMaps(emits), ].join('\n\n') } @@ -27,7 +26,7 @@ export function generateClientFindAPI(resolver: Resolver, libClient: string): st assert(queryBox && queryBox.t === 'enum') const iterableQueryMethods = queryBox.variants.map((x) => { - const { payload, predicate, selector } = match(x.type) + const { payload } = match(x.type) .with( { t: 'local', @@ -51,16 +50,14 @@ export function generateClientFindAPI(resolver: Resolver, libClient: string): st assert(x.tag.startsWith('Find')) const methodName = camelCase(x.tag.slice('Find'.length)) - const payloadArg = payload ? `payload: dm.${payload}, ` : '' - const payloadArgValue = payload ? `payload` : `null` + const payloadArg = payload ? `payload: types.${payload}, ` : '' + const payloadArgValue = payload ? `payload, ` : `` return ( - `/**\n* Convenience method for \`${x.tag}\` query, a variant of {@link dm.QueryBox} enum.\n` + - `* - Predicate type: {@link dm.${predicate}}\n` + - `* - Selector type: {@link dm.${selector}}\n */\n` + - ` public ${methodName}>(${payloadArg}params?: P): ` + - `client.QueryHandle> {` + - `return client.buildQueryHandle(this._executor, '${x.tag}', ${payloadArgValue}, params) }\n` + ` /** Convenience method for \`${x.tag}\` query, a variant of {@linkcode types.QueryBox} enum. */\n` + + ` public ${methodName}(${payloadArg}params?: core.QueryBuilderParams): ` + + `client.QueryBuilder<'${x.tag}'> {\n` + + ` return new client.QueryBuilder(this._executor, '${x.tag}', ${payloadArgValue}params)\n }\n` ) }) @@ -74,16 +71,16 @@ export function generateClientFindAPI(resolver: Resolver, libClient: string): st // const predicateType = return ( - `\n/** Convenience method for \`${x.tag}\` query, a variant of {@link dm.SingularQueryBox} enum. */` + - ` public ${methodName}(): Promise> {` + - `return client.executeSingularQuery(this._executor, '${x.tag}') }` + ` /** Convenience method for \`${x.tag}\` query, a variant of {@linkcode types.SingularQueryBox} enum. */\n` + + ` public ${methodName}(): Promise> {\n` + + ` return client.executeSingularQuery(this._executor, '${x.tag}')\n }\n` ) }) return [ `import * as client from '${libClient}'`, `import type * as core from '@iroha/core'`, - `import type * as dm from '@iroha/core/data-model'`, + `import type * as types from '@iroha/core/data-model'`, `export class FindAPI {`, ` private _executor: client.QueryExecutor`, ` public constructor(executor: client.QueryExecutor) { this._executor = executor; }`, @@ -126,6 +123,7 @@ export interface EmitEnumVariant { export type TypeRef = | { t: 'local'; id: Ident; params?: TypeRef[]; lazy?: boolean } | { t: 'lib'; id: LibType; params?: TypeRef[] } + | { t: 'lib-any'; id: Ident; params?: TypeRef[] } | { t: 'lib-array'; len: number; type: TypeRef } | { t: 'lib-b-tree-set-with-cmp'; type: TypeRef; compareFn: string } | { t: 'param'; index: number } @@ -1004,22 +1002,25 @@ function renderRef(ref: TypeRef): RefRender { valueId: lazy ? undefined : id, } }) - .with({ t: 'lib', params: P.array() }, ({ id, params }) => { - const typeGenerics = `<${params.map((x) => renderRef(x).type).join(', ')}>` - const valueId = `lib.${id}.with(${params.map((x) => renderRef(x).codec).join(', ')})` + .with( + { t: P.union('lib', 'lib-any') }, + ({ id, params }) => { + if (params?.length) { + const typeGenerics = `<${params.map((x) => renderRef(x).type).join(', ')}>` + const valueId = `lib.${id}.with(${params.map((x) => renderRef(x).codec).join(', ')})` - return { - type: `lib.${id}${typeGenerics}`, - codec: valueId, - } - }) - .with({ t: 'lib' }, ({ id }) => { - return { - type: `lib.${id}`, - codec: renderGetCodec(`lib.${id}`), - valueId: `lib.${id}`, - } - }) + return { + type: `lib.${id}${typeGenerics}`, + codec: valueId, + } + } + return { + type: `lib.${id}`, + codec: renderGetCodec(`lib.${id}`), + valueId: `lib.${id}`, + } + }, + ) .with({ t: 'lib-array' }, () => { throw new Error('This type of reference exists on pre-render stage only, really') }) @@ -1285,38 +1286,6 @@ function takeSelectorTypeName(selector: string): string | null { return selector.slice(0, -SELECTOR_SUFFIX.length) } -function buildQuerySelectorMapEntries(emits: EmitsMap): { query: string; selector: string }[] { - const schema = emits.get('QueryBox') - assert(schema && schema.t === 'enum') - return schema.variants.map((variant) => { - const selector = match(variant.type) - .with( - { - t: 'local', - id: 'QueryWithFilter', - params: [P._, P._, { t: 'lib', id: 'Vec', params: [{ t: 'local', id: P.select() }] }], - }, - (x) => takeSelectorTypeName(x), - ) - .otherwise(() => fail('unexpected query box value')) - - assert(selector) - - return { - query: variant.tag, - selector, - } - }) -} - -function expectNestedSelector(type: TypeRef) { - assert(type.t === 'local') - const selector = takeSelectorTypeName(type.id) - assert(selector) - if (selector === 'MetadataKey') return 'Json' - return selector -} - /** * _Most_ of the selectors match directly with the variant in `QueryOutputBatchBox`. * For example, `AccountProjectionSelector` (selector = `Account`) matches directly with @@ -1335,50 +1304,253 @@ function resolveSelectorAtomOutputBoxTag(selector: string) { .otherwise(() => selector) } -function buildSelectorOutputMapEntries( - emits: EmitsMap, -): { selector: string; entries: { name: string; value: string }[] }[] { - return [...emits].reduce( - (acc, [key, value]) => { - const selector = takeSelectorTypeName(key) - if (selector && selector !== 'MetadataKey') { - assert(value.t === 'enum', `not an enum: ${selector} (selector)`) - acc.push({ - selector, - entries: value.variants.map((variant) => ({ - name: variant.tag, - value: variant.tag === 'Atom' - ? resolveSelectorAtomOutputBoxTag(selector) - : expectNestedSelector(variant.type), - })), - }) +type PredicateTreeEntry = { + t: 'nested' + nested: PredicateTree +} | { + t: 'fn' + args: { name: string; type: TypeRef }[] + out: { t: 'nested'; tree: PredicateTree } | { t: 'final'; ref: TypeRef } +} + +type PredicateTree = Map + +function buildPredicateTree(emits: EmitsMap, id: string, start: string = id): PredicateTree { + const emit = emits.get(id) + assert(emit?.t === 'enum', `${id} is not a enum?`) + + function* expandFinal(atom: TypeRef): Generator<[string, PredicateTreeEntry]> { + assert(atom.t === 'local') + const emit = emits.get(atom.id) + assert(emit?.t === 'enum') + for (const i of emit.variants) { + const tagCamel = camelCase(i.tag) + const args = i.type.t === 'null' ? [] : [{ name: 'value', type: localRefToLibRef(i.type) }] + yield [tagCamel, { t: 'fn', args, out: { t: 'final', ref: { t: 'lib-any', id: start } } }] + } + } + + function* mapVariants(): Generator<[string, PredicateTreeEntry]> { + assert(emit?.t === 'enum') + if (!emit.variants.length) return + const [atom, ...delegates] = emit.variants + assert(atom.tag === 'Atom') + yield* expandFinal(atom.type) + for (const { tag, type } of delegates) { + const tagCamel = camelCase(tag) + assert(type.t === 'local') + if (type.id.startsWith('MetadataKeyProjection')) { + const emit = emits.get(type.id) + assert(emit?.t === 'struct') + const [key, projection] = emit.fields + assert(projection?.type?.t === 'local') + const tree = buildPredicateTree(emits, projection.type.id, start) + yield [tagCamel, { t: 'fn', args: [key], out: { t: 'nested', tree } }] + return } + + yield [tagCamel, { t: 'nested', nested: buildPredicateTree(emits, type.id, start) }] + } + } + + return new Map(mapVariants()) +} + +type BuildTreeAcc = { root: string; tags: string[] } + +const BuildTreeAcc = { + create: (root: string): BuildTreeAcc => { + const ty = takeSelectorTypeName(root) + assert(ty) + return { root: ty, tags: [] } + }, + push: (acc: BuildTreeAcc, tag: string): BuildTreeAcc => { + return { ...acc, tags: [...acc.tags, tag] } + }, + selectorId: (acc: BuildTreeAcc): string => { + const chain = [acc.root, ...acc.tags] + return chain.map((x) => toKebabCase(x)).join('-') + }, +} + +type SelectorTreeChild = { t: 'plain'; field: string; tree: SelectorTree } | { + t: 'fn' + field: string + args: { name: string; type: TypeRef }[] + tree: SelectorTree +} + +type SelectorTree = { id: string; output: TypeRef; children: SelectorTreeChild[] } + +function localRefToLibRef(ref: TypeRef): TypeRef { + function mapParams(params?: TypeRef[]) { + return (params ?? []).map((x) => localRefToLibRef(x)) + } + + if (ref.t === 'local') return { ...ref, t: 'lib-any', params: mapParams(ref.params) } + if (ref.t === 'lib') return { ...ref, params: mapParams(ref.params) } + // cover more if needed + return ref +} + +function buildSelectorTree(emits: EmitsMap, id: string, acc: BuildTreeAcc): SelectorTree { + const emit = emits.get(id) + + return match(emit) + .with({ t: 'enum' }, ({ variants }): SelectorTree => { + assertObjectMatch(variants[0], { tag: 'Atom', type: { t: 'null' } }) + const children = variants.slice(1).map(({ tag, type }): SelectorTreeChild => { + const tagCamel = camelCase(tag) + assert(type.t === 'local') + + if (type.id.startsWith('MetadataKeyProjection')) { + const emit = emits.get(type.id) + assert(emit?.t === 'struct') + const [key, projection] = emit.fields + assert(projection?.type?.t === 'local') + const tree = buildSelectorTree(emits, projection.type.id, BuildTreeAcc.push(acc, 'key')) + return { t: 'fn', field: tagCamel, args: [key], tree } + } + + return { t: 'plain', field: tagCamel, tree: buildSelectorTree(emits, type.id, BuildTreeAcc.push(acc, tag)) } + }) + const selectorType = takeSelectorTypeName(id) + assert(selectorType) + + const outputTag = resolveSelectorAtomOutputBoxTag(selectorType) + const outputBox = emits.get('QueryOutputBatchBox') + assert(outputBox?.t === 'enum') + const outputVar = outputBox.variants.find((x) => x.tag === outputTag) + assert(outputVar) + const outputVec = outputVar.type + assert(outputVec.t === 'lib' && outputVec?.params) + const output = localRefToLibRef(outputVec.params[0]) + + return { + id: BuildTreeAcc.selectorId(acc), + output, + children, + } + }) + .otherwise((other) => { + fail(`Could not match ${id}: ${Deno.inspect(other)}`) + }) +} + +function collectSelectorTreeIds(tree: SelectorTree): Set { + function* visit(tree: SelectorTree): Generator { + yield tree.id + for (const i of tree.children) { + yield* visit(i.tree) + } + } + + return new Set(visit(tree)) +} + +function collectSelectorOutputs(tree: SelectorTree): Map { + function* visit(tree: SelectorTree): Generator<[id: string, output: TypeRef]> { + yield [tree.id, tree.output] + for (const i of tree.children) { + yield* visit(i.tree) + } + } + + return new Map(visit(tree)) +} + +function renderSelectorTree(tree: SelectorTree, indent: string): string { + const children = tree.children.map((child) => { + if (child.t === 'plain') return `${indent} ${child.field}: ${renderSelectorTree(child.tree, indent + ' ')}\n` + const args = child.args.map((x) => `${x.name}: ${renderRef(x.type).type}`).join(', ') + return `${indent} ${child.field}(${args}): ${renderSelectorTree(child.tree, indent + ' ')}\n` + }).join('') + + return `{\n${indent} __selector: '${tree.id}',\n${children}${indent}}` +} + +function renderPredicateTree(tree: PredicateTree, indent: string): string { + if (!tree.size) return 'never' + + function* iter() { + for (const [id, entry] of tree) { + if (entry.t === 'nested') { + yield `${id}: ${renderPredicateTree(entry.nested, indent + ' ')}` + } else { + const output = entry.out.t === 'nested' + ? renderPredicateTree(entry.out.tree, indent + ' ') + : renderRef(entry.out.ref).type + const args = entry.args.map((x) => `${x.name}: ${renderRef(x.type).type}`).join(', ') + yield `${id}: (${args}) => ${output}` + } + } + } + + const items = [...iter()].map((x) => `${indent} ${x}`).join('\n') + return `{\n${items}\n${indent}}` +} + +export function generatePrototypes(resolver: Resolver, lib: string) { + const box = resolver.emits.get('QueryBox') + assert(box && box.t === 'enum') + + const projections = box.variants.reduce( + (acc, variant) => { + const query = variant.tag + assert(variant.type.t === 'local' && variant.type.id === 'QueryWithFilter' && variant.type.params) + const [_payload, compound, selectorTuple] = variant.type.params + assert(compound.t === 'lib' && compound.id === 'CompoundPredicate' && compound.params?.length === 1) + const predicate = compound.params[0] + assert(selectorTuple.t === 'lib' && selectorTuple.id === 'Vec' && selectorTuple.params?.length === 1) + const selector = selectorTuple.params[0] + assert(selector.t === 'local' && predicate.t === 'local') + + const selectorTree = buildSelectorTree(resolver.emits, selector.id, BuildTreeAcc.create(selector.id)) + const selectorIds = collectSelectorTreeIds(selectorTree) + const selectorOutputs = collectSelectorOutputs(selectorTree) + + const predicateTree = buildPredicateTree(resolver.emits, predicate.id) + + acc.push({ query, predicate: predicateTree, selector: selectorTree, selectorIds, selectorOutputs }) + return acc }, - [] as ReturnType, + [] as { + query: string + predicate: PredicateTree + selector: SelectorTree + selectorIds: Set + selectorOutputs: Map + }[], ) -} -/** - * Properties of the output to test: - * - all values in query output map point to some key in the selector output map - * - all `Atom` variants for each selector in selector output map point to some variant of - * query output batch box - */ -function generateQueryMaps(emits: EmitsMap): string[] { - const querySelectorMap = buildQuerySelectorMapEntries(emits) - .map((x) => `${x.query}: '${x.selector}'`) - .join('; ') - - const selectorOutputMap = buildSelectorOutputMapEntries(emits) - .map((x) => { - const inner = x.entries.map((y) => `${y.name}: '${y.value}'`).join(';') - return `${x.selector}: {\n${inner} }` - }) - .join('; ') + const compatEntries = projections.map((x) => ` ${x.query}: ${[...x.selectorIds].map((y) => `'${y}'`).join(' | ')}`) + .join('\n') + const querySelectorCompat = `export type QueryCompatibleSelectors = {\n${compatEntries}\n}` + + const outputEntries = [ + ...projections.flatMap((x) => [...x.selectorOutputs]) + .reduce((acc, [id, output]) => { + if (acc.has(id)) assertEquals(acc.get(id), output) + else acc.set(id, output) + return acc + }, new Map()), + ] + .map(([id, output]) => ` '${id}': ${renderRef(output).type}`) + .join('\n') + const querySelectorOutput = `export type SelectorIdToOutput = {\n${outputEntries}\n}` + + const selectorsEntries = projections.map((x) => ` ${x.query}: ${renderSelectorTree(x.selector, ' ')}`).join('\n') + const selectors = `export type QuerySelectors = {\n${selectorsEntries}\n}` + + const predicatesEntries = projections.map((x) => ` ${x.query}: ${renderPredicateTree(x.predicate, ' ')}`).join('\n') + const predicates = `export type QueryPredicates = {\n${predicatesEntries}\n}` return [ - `export type QuerySelectorMap = { ${querySelectorMap} }`, - `export type SelectorOutputMap = { ${selectorOutputMap} }`, - ] + `import type * as lib from '${lib}'`, + querySelectorCompat, + querySelectorOutput, + selectors, + predicates, + ].join('\n\n') } diff --git a/etc/codegen_test.ts b/etc/codegen_test.ts new file mode 100644 index 00000000..f4fc93b4 --- /dev/null +++ b/etc/codegen_test.ts @@ -0,0 +1,143 @@ +import { assertSnapshot } from '@std/testing/snapshot' +import { describe, test } from '@std/testing/bdd' +import { expect } from '@std/expect' +import { + type EmitCode, + type EmitsMap, + enumShortcuts, + type EnumShortcutTreeVariant, + generateClientFindAPI, + generatePrototypes, + renderShortcutsTree, + Resolver, +} from './codegen.ts' +import SCHEMA from '@iroha/core/data-model/schema-json' + +const resolver = new Resolver({ ...SCHEMA }) + +describe('generate prototypes', () => { + test('prototypes snapshot', async (t) => { + const output = generatePrototypes(resolver, 'prelude') + await assertSnapshot(t, output) + }) + + test('find api snapshot', async (t) => { + await assertSnapshot(t, generateClientFindAPI(resolver, 'prelude')) + }) +}) + +describe('enum shortcuts', () => { + const SAMPLE = { + A: { + t: 'enum', + variants: [ + { tag: 'Unit', discriminant: 0, type: { t: 'null' } }, + { tag: 'WithType', type: { t: 'local', id: 'Whichever' }, discriminant: 1 }, + { tag: 'Nested', type: { t: 'local', id: 'B' }, discriminant: 2 }, + ], + }, + B: { + t: 'enum', + variants: [ + { tag: 'Bunit', discriminant: 0, type: { t: 'null' } }, + { discriminant: 1, tag: 'Bnested', type: { t: 'local', id: 'C' } }, + ], + }, + C: { + t: 'enum', + variants: [ + { tag: 'CUnit', discriminant: 0, type: { t: 'null' } }, + { discriminant: 1, tag: 'Cfinal', type: { t: 'local', id: 'Whichever' } }, + ], + }, + Whichever: { t: 'alias', to: { t: 'lib', id: 'AccountId' } }, + } satisfies Record + + const SAMPLE_MAP = new Map(Object.entries(SAMPLE)) + + test('build shortcuts tree', (t) => { + const tree = enumShortcuts(SAMPLE.A.variants, SAMPLE_MAP) + assertSnapshot(t, tree) + }) + + test('build: takes aliases into consideration', () => { + const map: EmitsMap = new Map([ + ['Foo', { t: 'alias', to: { t: 'local', id: 'Bar' } }], + ['Bar', { t: 'enum', variants: [{ discriminant: 0, tag: 'Baz', type: { t: 'null' } }] }], + ]) + + const tree = enumShortcuts([{ discriminant: 0, tag: 'Bar', type: { t: 'local', id: 'Foo' } }], map) + + expect(tree).toEqual( + [ + { + 'name': 'Bar', + 't': 'enum', + 'tree': { + 'id': 'Foo', + 'variants': [ + { + 'name': 'Baz', + 't': 'unit', + }, + ], + }, + }, + ], + ) + }) + + test('build: omits enums without variants', () => { + const map: EmitsMap = new Map([ + ['Foo', { t: 'enum', variants: [] }], + [ + 'Bar', + { + t: 'enum', + variants: [ + { discriminant: 0, tag: 'Baz', type: { t: 'null' } }, + { discriminant: 1, tag: 'Foo', type: { t: 'local', id: 'Foo' } }, + ], + }, + ], + // nested void + ['Void0', { t: 'enum', variants: [{ discriminant: 0, tag: 'Void1', type: { t: 'local', id: 'Void1' } }] }], + ['Void1', { t: 'enum', variants: [{ discriminant: 0, tag: 'Foo', type: { t: 'local', id: 'Foo' } }] }], + ]) + + const tree = enumShortcuts( + [ + { discriminant: 0, tag: 'foo', type: { t: 'local', id: 'Foo' } }, + { discriminant: 1, tag: 'bar', type: { t: 'local', id: 'Bar' } }, + { discriminant: 2, tag: 'void', type: { t: 'local', id: 'Void0' } }, + ], + map, + ) + + expect(tree).toEqual( + [ + { + name: 'bar', + t: 'enum', + tree: { + id: 'Bar', + variants: [{ name: 'Baz', t: 'unit' }], + }, + }, + ] satisfies EnumShortcutTreeVariant[], + ) + }) + + test('generate shortcut tree', async (t) => { + const TREE = { id: 'A', variants: enumShortcuts(SAMPLE.A.variants, SAMPLE_MAP) } + const type = renderShortcutsTree(TREE, 'type') + const value = renderShortcutsTree(TREE, 'value') + + const full = ` + type test = ${type} + const test = ${value} + ` + + await assertSnapshot(t, full) + }) +}) diff --git a/etc/task-codegen.ts b/etc/task-codegen.ts index bc6a6fce..79fafb5f 100644 --- a/etc/task-codegen.ts +++ b/etc/task-codegen.ts @@ -1,35 +1,44 @@ import type { Schema } from '@iroha/core/data-model/schema' import SCHEMA from '@iroha/core/data-model/schema-json' -import { generateClientFindAPI, generateDataModel, Resolver } from './codegen.ts' -import $ from 'jsr:@david/dax' -import { expect } from 'jsr:@std/expect' -import * as dprint from 'jsr:@dprint/formatter' -import * as dprintTS from 'npm:@dprint/typescript' +import { generateClientFindAPI, generateDataModel, generatePrototypes, Resolver } from './codegen.ts' +import { expect } from '@std/expect' import * as colors from '@std/fmt/colors' +import { parseArgs } from 'jsr:@std/cli/parse-args' +import { assertEquals } from '@std/assert/equals' +import { fail } from '@std/assert/fail' -const tsFormatter = dprint.createFromBuffer(await Deno.readFile(dprintTS.getPath())) -const formatTS = (code: string) => { - console.time('dprint') - const text = tsFormatter.formatText({ filePath: 'file.ts', fileText: code }) - console.timeEnd('dprint') - return text -} +const args = parseArgs(Deno.args, { + boolean: ['update'], +}) + +async function write(entry: { file: string; code: () => string }) { + const logPre = `${colors.gray(entry.file + ' =>')} ` + + const code = entry.code() -async function write({ file, code }: { file: string; code: string }) { - let status: string try { - const prevCode = await Deno.readTextFile(file) + const prevCode = await Deno.readTextFile(entry.file) if (prevCode === code) { - status = 'unchanged' + console.log(logPre + colors.green('unchanged')) } else { - await Deno.writeTextFile(file, code) - status = 'updated' + if (!args.update) { + assertEquals(code, prevCode, 'codegen changed (overwrite with --update)') + fail('previous always fails') + } + await Deno.writeTextFile(entry.file, code) + console.log(logPre + colors.blue('updated')) } - } catch { - await Deno.writeTextFile(file, code) - status = 'created' + } catch (err) { + if (!(err instanceof Deno.errors.NotFound)) throw err + await Deno.writeTextFile(entry.file, code) + console.log(logPre + colors.green('created')) + } +} + +async function writeAll(entries: { file: string; code: () => string }[]) { + for (const i of entries) { + await write(i) } - $.logStep(`Generated ${colors.cyan(file)} (${status})`) } /** @@ -58,12 +67,19 @@ expect(Object.keys(SCHEMA)).not.toContain(Object.keys(EXTENSION)) const resolver = new Resolver({ ...SCHEMA, ...EXTENSION }) -await write({ - file: 'packages/core/data-model/_generated_.ts', - code: formatTS(generateDataModel(resolver, './_generated_.prelude.ts')), -}) - -await write({ - file: 'packages/client/find-api._generated_.ts', - code: formatTS(generateClientFindAPI(resolver, './find-api._generated_.prelude.ts')), -}) +console.time('codegen') +await writeAll([ + { + file: 'packages/core/data-model/prototypes.generated.ts', + code: () => generatePrototypes(resolver, './prototypes.generated.prelude.ts'), + }, + { + file: 'packages/core/data-model/types.generated.ts', + code: () => generateDataModel(resolver, './types.generated.prelude.ts'), + }, + { + file: 'packages/client/find-api.generated.ts', + code: () => generateClientFindAPI(resolver, './find-api.generated.prelude.ts'), + }, +]) +console.timeEnd('codegen') diff --git a/packages/client/client.spec.ts b/packages/client/client.spec.ts deleted file mode 100644 index e43bb033..00000000 --- a/packages/client/client.spec.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { describe, test } from 'vitest' - -describe('transaction', () => { - // I don't know how to test it; delayed - test.todo('verify abort signal works') -}) diff --git a/packages/client/client.ts b/packages/client/client.ts index 8a45b20b..5a2fc74d 100644 --- a/packages/client/client.ts +++ b/packages/client/client.ts @@ -13,7 +13,7 @@ import { WebSocketAPI, } from './api-ws.ts' import type { IsomorphicWebSocketAdapter } from './web-socket/mod.ts' -import { FindAPI } from './find-api._generated_.ts' +import { FindAPI } from './find-api.generated.ts' import { QueryExecutor } from './query.ts' export { FindAPI } diff --git a/packages/client/find-api._generated_.prelude.ts b/packages/client/find-api._generated_.prelude.ts deleted file mode 100644 index f355b208..00000000 --- a/packages/client/find-api._generated_.prelude.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - buildQuery, - type BuildQueryParams, - type GetQueryPayload, - type GetSingularQueryOutput, - type QueryKind, - type SingularQueryKind, -} from '@iroha/core' -import { type QueryExecutor, QueryHandle } from './query.ts' - -export * from './query.ts' - -export function buildQueryHandle( - executor: QueryExecutor, - kind: K, - payload: GetQueryPayload, - params?: BuildQueryParams, -): QueryHandle { - const query = buildQuery(kind, payload, params) - return new QueryHandle(query, executor) -} - -export async function executeSingularQuery( - executor: QueryExecutor, - kind: K, -): Promise> { - const result = await executor.executeSingular({ kind }) - return result.value as GetSingularQueryOutput -} diff --git a/packages/client/find-api.generated.prelude.ts b/packages/client/find-api.generated.prelude.ts new file mode 100644 index 00000000..cedb3b0b --- /dev/null +++ b/packages/client/find-api.generated.prelude.ts @@ -0,0 +1,12 @@ +import type { GetSingularQueryOutput, SingularQueryKind } from '@iroha/core' +import type { QueryExecutor } from './query.ts' + +export * from './query.ts' + +export async function executeSingularQuery( + executor: QueryExecutor, + kind: K, +): Promise> { + const result = await executor.executeSingular({ kind }) + return result.value as GetSingularQueryOutput +} diff --git a/packages/client/mod.ts b/packages/client/mod.ts index ca20e21d..668919cf 100644 --- a/packages/client/mod.ts +++ b/packages/client/mod.ts @@ -145,12 +145,12 @@ * async function test(client: Client) { * const accounts: types.Account[] = await client.find * .accounts({ - * predicate: types.CompoundPredicate.Atom( - * types.AccountProjectionPredicate.Id.Domain.Name.Atom.EndsWith('land') - * ), * offset: 10, * limit: new types.NonZero(50), * }) + * .filterWith((account) => + * types.CompoundPredicate.Atom(account.id.domain.name.endsWith('land')) + * ) * .executeAll() * } * ``` @@ -170,12 +170,8 @@ * async function test(client: Client) { * // use selectors and pagination * const items: [types.Hash, types.AccountId][] = await client.find - * .transactions({ - * selector: [ - * types.CommittedTransactionProjectionSelector.BlockHash.Atom, - * types.CommittedTransactionProjectionSelector.Value.Authority.Atom, - * ], - * }) + * .transactions() + * .selectWith((tx) => [tx.blockHash, tx.value.authority]) * .executeAll() * } * ``` diff --git a/packages/client/query.ts b/packages/client/query.ts index f6d619ff..9c813761 100644 --- a/packages/client/query.ts +++ b/packages/client/query.ts @@ -1,25 +1,33 @@ -import { type BuildQueryResult, signQuery } from '@iroha/core' -import * as dm from '@iroha/core/data-model' +import { + type DefaultQueryOutput, + QueryBuilder as BaseQueryBuilder, + type QueryBuilderCtorArgs, + type QueryKind, + type SelectedTuple, + signQuery, +} from '@iroha/core' +import * as types from '@iroha/core/data-model' import type { PrivateKey } from '@iroha/core/crypto' import { assert } from '@std/assert' import type { MainAPI } from './api.ts' +import type { QuerySelectors } from '../core/data-model/prototypes.generated.ts' export class QueryExecutor { private readonly api: MainAPI - private readonly authority: dm.AccountId + private readonly authority: types.AccountId private readonly privateKey: PrivateKey - public constructor(api: MainAPI, authority: dm.AccountId, authorityPrivateKey: PrivateKey) { + public constructor(api: MainAPI, authority: types.AccountId, authorityPrivateKey: PrivateKey) { this.api = api this.authority = authority this.privateKey = authorityPrivateKey } - public async *execute(query: dm.QueryWithParams): AsyncGenerator { - let continueCursor: dm.ForwardCursor | null = null + public async *execute(query: types.QueryWithParams): AsyncGenerator { + let continueCursor: types.ForwardCursor | null = null do { const response = await this.api.query( - this.signQuery(continueCursor ? dm.QueryRequest.Continue(continueCursor) : dm.QueryRequest.Start(query)), + this.signQuery(continueCursor ? types.QueryRequest.Continue(continueCursor) : types.QueryRequest.Start(query)), ) assert(response.kind === 'Iterable') @@ -29,24 +37,30 @@ export class QueryExecutor { } while (continueCursor) } - public async executeSingular(query: dm.SingularQueryBox): Promise { + public async executeSingular(query: types.SingularQueryBox): Promise { const response = await this.api.query(this.signQuery({ kind: 'Singular', value: query })) assert(response.kind === 'Singular') return response.value } - public signQuery(request: dm.QueryRequest): dm.SignedQuery { + public signQuery(request: types.QueryRequest): types.SignedQuery { return signQuery({ request, authority: this.authority }, this.privateKey) } } -export class QueryHandle { - private readonly _query: BuildQueryResult - private readonly _executor: QueryExecutor +export class QueryBuilder> extends BaseQueryBuilder { + #executor: QueryExecutor - public constructor(query: BuildQueryResult, executor: QueryExecutor) { - this._query = query - this._executor = executor + public constructor(executor: QueryExecutor, ...args: QueryBuilderCtorArgs) { + super(...args) + this.#executor = executor + } + + public override selectWith( + fn: (prototype: QuerySelectors[Q]) => ProtoTuple, + ): QueryBuilder> { + super.selectWith(fn) + return this as QueryBuilder> } public async executeAll(): Promise { @@ -70,8 +84,8 @@ export class QueryHandle { } public async *batches(): AsyncGenerator { - for await (const { batch } of this._executor.execute(this._query.query)) { - const items = [...this._query.parseResponse(batch)] + for await (const { batch } of this.#executor.execute(this.build())) { + const items = [...this.parseOutput(batch)] yield items } } diff --git a/packages/client/test/queries.spec-d.ts b/packages/client/test/queries.spec-d.ts index eae87d9d..bf4b1d2a 100644 --- a/packages/client/test/queries.spec-d.ts +++ b/packages/client/test/queries.spec-d.ts @@ -1,60 +1,62 @@ // deno-lint-ignore-file no-unused-vars -import * as dm from '@iroha/core/data-model' -import type { Client, QueryHandle } from '@iroha/client' +import * as types from '@iroha/core/data-model' +import { type Client, QueryBuilder, type QueryExecutor } from '@iroha/client' type Expect = T type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false +declare const executor: QueryExecutor declare const client: Client -type QueryHandleOutput> = Handle extends QueryHandle ? T : never +const selectPeerKeys = await new QueryBuilder(executor, 'FindPeers') + .filterWith((proto) => types.CompoundPredicate.Atom(proto.publicKey.equals(types.PublicKey.fromMultihash('test')))) + .selectWith((proto) => proto.publicKey) + .executeAll() +type test_select_peer_keys = Expect> -// const findAccs = client.query('FindAccounts') -// type test_find_accounts_output = Expect, dm.Account>> +const selectAccountMulti = await new QueryBuilder(executor, 'FindAccounts', { + offset: 5n, + limit: new types.NonZero(5), +}) + .selectWith(( + account, + ) => [ + account.id.domain.name, + account.metadata.key(new types.Name('key1')), + account.metadata.key(new types.Name('key2')), + ]) + .executeAll() +type test_select_account_multi = Expect> + +type BuilderOutput> = Handle extends QueryBuilder ? T : never const findAccs = client.find.accounts() -type test_find_accounts_output = Expect, dm.Account>> +type test_find_accounts_output = Expect, types.Account>> -const findAccsWithFilter = client.find.accounts({ - predicate: dm.CompoundPredicate.Atom( - dm.AccountProjectionPredicate.Id.Domain.Name.Atom.Contains('alice'), - ), -}) -type test_find_accs_with_filter = Expect, dm.Account>> +const findAccsWithFilter = client.find.accounts() + .filterWith((proto) => types.CompoundPredicate.Atom(proto.id.domain.name.contains('alice'))) +type test_find_accs_with_filter = Expect, types.Account>> -const findAccsWithSelector = client.find.accounts({ - selector: dm.AccountProjectionSelector.Atom, -}) -type test_find_accs_selector = Expect, dm.Account>> +const findAccsWithSelector = client.find.accounts().selectWith((x) => x) +type test_find_accs_selector = Expect, types.Account>> -const findAccsWithSelector2 = client.find.accounts({ - selector: [dm.AccountProjectionSelector.Atom], -}) -type test_find_accs_selector2 = Expect, dm.Account>> +const findAccsWithSelector2 = client.find.accounts().selectWith((x) => [x]) +type test_find_accs_selector2 = Expect, types.Account>> -const findAccsWithSelector3 = client.find.accounts({ - selector: [dm.AccountProjectionSelector.Id.Signatory.Atom, dm.AccountProjectionSelector.Id.Domain.Name.Atom], -}) +const findAccsWithSelector3 = client.find.accounts() + .selectWith((acc) => [acc.id.signatory, acc.id.domain.name]) type test_find_accs_selector3 = Expect< - Equal, [dm.PublicKey, dm.Name]> + Equal, [types.PublicKey, types.Name]> > const accountsExecuteAll = await client.find.accounts().executeAll() -type test_accs_exec_all = Expect> - -const findBlockHeaderHashes = client.find.blockHeaders({ selector: dm.BlockHeaderProjectionSelector.Hash.Atom }) -type test_block_header_hashes = Expect, dm.Hash>> +type test_accs_exec_all = Expect> -const findDomainsMetadata = client.find.domains({ selector: dm.DomainProjectionSelector.Metadata.Atom }) -const findAccountsMetadata = client.find.accounts({ - select: dm.DomainProjectionSelector.Metadata.Atom, - filter: '', - predicate: dm.CompoundPredicate.Atom(dm.AccountProjectionPredicate.Id.Domain.Name.Atom.Contains('test')), -}) +const findBlockHeaderHashes = client.find.blockHeaders().selectWith((x) => x.hash) +type test_block_header_hashes = Expect, types.Hash>> -const testExtraFields = client.find.accounts({ - predicate: dm.CompoundPredicate.Atom(dm.AccountProjectionPredicate.Id.Domain.Name.Atom.EndsWith('test')), - filter: '', - select: 12, -}) +const findDomainsMetadata = client.find.domains().selectWith((x) => x.metadata) +const findAccountsMetadata = client.find.accounts() + .selectWith((x) => x.metadata) + .filterWith((x) => types.CompoundPredicate.Atom(x.id.domain.name.contains('test'))) diff --git a/packages/core/crypto/mod.ts b/packages/core/crypto/mod.ts index a70e7be7..3cd92774 100644 --- a/packages/core/crypto/mod.ts +++ b/packages/core/crypto/mod.ts @@ -1,7 +1,7 @@ /** * Port of `iroha_crypto` Rust crate via WebAssembly. * - * ### Compatibility + * ## Compatibility * * This package uses native `.wasm` ES Module imports, **which isn't widely supported yet**. * However, there are some tricks in play to increase the compatibility. diff --git a/packages/core/data-model/mod.ts b/packages/core/data-model/mod.ts index 4899c627..453b32f8 100644 --- a/packages/core/data-model/mod.ts +++ b/packages/core/data-model/mod.ts @@ -75,6 +75,6 @@ export * from './primitives.ts' export * from './compound.ts' -export * from './_generated_.ts' +export * from './types.generated.ts' export { Hash, KeyPair, PrivateKey, PublicKey, Signature } from '../crypto/mod.ts' export type { Variant, VariantUnit } from '../util.ts' diff --git a/packages/core/data-model/prototypes.generated.prelude.ts b/packages/core/data-model/prototypes.generated.prelude.ts new file mode 100644 index 00000000..e804aedd --- /dev/null +++ b/packages/core/data-model/prototypes.generated.prelude.ts @@ -0,0 +1,2 @@ +export * from './types.generated.prelude.ts' +export * from './types.generated.ts' diff --git a/packages/core/data-model/_generated_.prelude.ts b/packages/core/data-model/types.generated.prelude.ts similarity index 100% rename from packages/core/data-model/_generated_.prelude.ts rename to packages/core/data-model/types.generated.prelude.ts diff --git a/packages/core/query-internal.test.ts b/packages/core/query-internal.test.ts new file mode 100644 index 00000000..901c2249 --- /dev/null +++ b/packages/core/query-internal.test.ts @@ -0,0 +1,134 @@ +import { describe, test } from '@std/testing/bdd' +import { expect } from '@std/expect' +import { getActualSelector, predicateProto, selectorProto } from './query-internal.ts' +import * as dm from './data-model/mod.ts' + +describe('predicateProto()', () => { + function compare(actual: T, expected: T) { + expect(actual).toEqual(expected) + } + + const SAMPLE_ACC = new dm.AccountId(dm.KeyPair.random().publicKey(), new dm.Name('www')) + + test('FindDomains', () => { + const proto = predicateProto<'FindDomains'>() + + compare( + predicateProto<'FindDomains'>().id.name.endsWith('test'), + dm.DomainProjectionPredicate.Id.Name.Atom.EndsWith('test'), + ) + + compare( + proto.id.equals(new dm.Name('test')), + dm.DomainProjectionPredicate.Id.Atom.Equals(new dm.Name('test')), + ) + + compare( + proto.metadata.key(new dm.Name('4412')).equals(dm.Json.fromValue([1, 2, 3])), + dm.DomainProjectionPredicate.Metadata.Key({ + key: new dm.Name('4412'), + projection: dm.JsonProjectionPredicate.Atom.Equals(dm.Json.fromValue([1, 2, 3])), + }), + ) + }) + + test('FindBlocks', () => { + const proto = predicateProto<'FindBlocks'>() + + compare( + proto.header.hash.equals(dm.Hash.zeroed()), + dm.SignedBlockProjectionPredicate.Header.Hash.Atom.Equals(dm.Hash.zeroed()), + ) + }) + + test('FindAssets', () => { + const proto = predicateProto<'FindAssets'>() + + compare( + proto.id.account.equals(SAMPLE_ACC), + dm.AssetProjectionPredicate.Id.Account.Atom.Equals(SAMPLE_ACC), + ) + + compare( + proto.id.account.signatory.equals(SAMPLE_ACC.signatory), + dm.AssetProjectionPredicate.Id.Account.Signatory.Atom.Equals(SAMPLE_ACC.signatory), + ) + + compare( + proto.value.isNumeric(), + dm.AssetProjectionPredicate.Value.Atom.IsNumeric, + ) + }) + + test('FindTransactions', () => { + const proto = predicateProto<'FindTransactions'>() + + compare( + proto.error.isSome(), + dm.CommittedTransactionProjectionPredicate.Error.Atom.IsSome, + ) + + compare( + proto.blockHash.equals(dm.Hash.zeroed()), + dm.CommittedTransactionProjectionPredicate.BlockHash.Atom.Equals(dm.Hash.zeroed()), + ) + + compare( + proto.value.authority.domain.name.contains('wuw'), + dm.CommittedTransactionProjectionPredicate.Value.Authority.Domain.Name.Atom.Contains('wuw'), + ) + }) + + test('metadata in different queries', () => { + compare( + predicateProto<'FindTriggers'>().action.metadata.key(new dm.Name('aaa')).equals(dm.Json.fromValue(false)), + dm.TriggerProjectionPredicate.Action.Metadata.Key({ + key: new dm.Name('aaa'), + projection: dm.JsonProjectionPredicate.Atom.Equals(dm.Json.fromValue(false)), + }), + ) + + compare( + predicateProto<'FindAccounts'>().metadata.key(new dm.Name('aaa')).equals(dm.Json.fromValue(false)), + dm.AccountProjectionPredicate.Metadata.Key({ + key: new dm.Name('aaa'), + projection: dm.JsonProjectionPredicate.Atom.Equals(dm.Json.fromValue(false)), + }), + ) + }) +}) + +describe('selectorProto()', () => { + function compare(selector: S, actual: unknown) { + expect(getActualSelector(selector)).toEqual(actual) + } + + test('FindDomains', () => { + const proto = selectorProto<'FindDomains'>() + + compare(proto, dm.DomainProjectionSelector.Atom) + compare(proto.id, dm.DomainProjectionSelector.Id.Atom) + compare(proto.id.name, dm.DomainProjectionSelector.Id.Name.Atom) + compare(proto.metadata, dm.DomainProjectionSelector.Metadata.Atom) + compare( + proto.metadata.key(new dm.Name('bbb')), + dm.DomainProjectionSelector.Metadata.Key({ key: new dm.Name('bbb'), projection: dm.JsonProjectionSelector.Atom }), + ) + }) + + test('FindTransactions', () => { + const proto = selectorProto<'FindTransactions'>() + + compare(proto, dm.CommittedTransactionProjectionSelector.Atom) + compare(proto.error, dm.CommittedTransactionProjectionSelector.Error.Atom) + compare( + proto.value.authority.domain.name, + dm.CommittedTransactionProjectionSelector.Value.Authority.Domain.Name.Atom, + ) + compare(proto.blockHash, dm.CommittedTransactionProjectionSelector.BlockHash.Atom) + }) + + test('errors when trying to get an actual selector from wrong value', () => { + expect(() => getActualSelector('whichever data')).toThrow('Expected a magical selector') + }) +}) diff --git a/packages/core/query-internal.ts b/packages/core/query-internal.ts new file mode 100644 index 00000000..e04f10d1 --- /dev/null +++ b/packages/core/query-internal.ts @@ -0,0 +1,140 @@ +import { assert, fail } from '@std/assert' +import type { QueryKind } from './query-types.ts' +import type * as prototypes from './data-model/prototypes.generated.ts' +import { Name } from './data-model/compound.ts' + +export const QUERIES_WITH_PAYLOAD = new Set( + ['FindAccountsWithAsset', 'FindPermissionsByAccountId', 'FindRolesByAccountId'] as const, +) + +// deno-lint-ignore ban-types +type JustFunction = Function + +type PredicateBuilderChain = string | { t: 'metadata-key'; key: Name } + +type PredicateBuilder = JustFunction & { + __props: PredicateBuilderChain[] +} + +function funWithProps(props: T): JustFunction & T { + function noop() {} + return Object.assign(noop, props) +} + +function makePredicateBuilder(): PredicateBuilder { + return funWithProps({ __props: [] }) +} + +function predicateBuilderPushProp(value: PredicateBuilder, prop: string): PredicateBuilder { + return funWithProps({ __props: [...value.__props, prop] }) +} + +function fixPropCase(prop: string): string { + return prop[0].toUpperCase() + prop.slice(1) +} + +function predicateBuilderCall( + value: PredicateBuilder, + args: unknown[], +): { t: 'final'; value: unknown } | { t: 'continue'; value: PredicateBuilder } { + assert(value.__props.length) + const arg = args.length === 0 ? null : args.length === 1 ? { some: args[0] } : fail('must be 0 or 1 args') + + if (value.__props.at(-1) === 'key' && arg && arg.some instanceof Name) { + return { + t: 'continue', + value: funWithProps({ + __props: [...value.__props, { t: 'metadata-key', key: arg.some }], + }), + } + } + + const lastProp = value.__props.at(-1)! + assert(typeof lastProp === 'string') + const lastPropFixed = fixPropCase(lastProp) + let acc: any = arg ? { kind: lastPropFixed, value: arg.some } : { kind: lastPropFixed } + acc = { kind: 'Atom', value: acc } + for (let i = value.__props.length - 2; i >= 0; i--) { + const prop = value.__props[i] + if (typeof prop === 'string') acc = { kind: fixPropCase(prop), value: acc } + else if (prop.t === 'metadata-key') acc = { key: prop.key, projection: acc } + else fail('unreachable') + } + + return { t: 'final', value: acc } +} + +function makePredicateBuilderProxy(builder: PredicateBuilder): unknown { + return new Proxy(builder, { + apply(target, _thisArg, args) { + const ret = predicateBuilderCall(target, args) + if (ret.t === 'continue') return makePredicateBuilderProxy(ret.value) + return ret.value + }, + get(target, p) { + assert(typeof p === 'string') + return makePredicateBuilderProxy(predicateBuilderPushProp(target, p)) + }, + }) +} + +export function predicateProto(): prototypes.QueryPredicates[Q] { + return makePredicateBuilderProxy(makePredicateBuilder()) as any +} + +const selector = Symbol('actual-selector') + +type SelectorBuilder = JustFunction & { __props: PredicateBuilderChain[] } + +function makeSelectorBuilder(): SelectorBuilder { + return makePredicateBuilder() +} + +function selectorBuilderPushProp(value: SelectorBuilder, prop: string): SelectorBuilder { + return predicateBuilderPushProp(value, prop) +} + +function selectorBuilderCall(value: SelectorBuilder, args: unknown[]): SelectorBuilder { + assert(value.__props.at(-1) === 'key' && args.length === 1) + const name = args[0] + assert(name instanceof Name) + return funWithProps({ __props: [...value.__props, { t: 'metadata-key', key: name }] }) +} + +function buildSelector(builder: SelectorBuilder): unknown { + let acc: any = { kind: 'Atom' } + for (let i = builder.__props.length - 1; i >= 0; i--) { + const prop = builder.__props[i] + if (typeof prop === 'string') acc = { kind: fixPropCase(prop), value: acc } + else if (prop.t === 'metadata-key') acc = { key: prop.key, projection: acc } + else fail('unreachable') + } + return acc +} + +function makeSelectorBuilderProxy(value: SelectorBuilder): unknown { + return new Proxy(value, { + get(target, p) { + if (p === selector) return buildSelector(target) + if (typeof p === 'string') { + return makeSelectorBuilderProxy(selectorBuilderPushProp(target, p)) + } + }, + apply(target, _thisArg, args) { + return makeSelectorBuilderProxy(selectorBuilderCall(target, args)) + }, + }) +} + +export function selectorProto(): prototypes.QuerySelectors[Q] { + return makeSelectorBuilderProxy(makeSelectorBuilder()) as any +} + +export function getActualSelector(builder: unknown): unknown { + if (typeof builder === 'function' && '__props' in builder) { + return (builder as any)[selector] + } + throw new TypeError( + 'Expected a magical selector builder type, got something else; make sure you are using `.selectWith()` properly.', + ) +} diff --git a/packages/core/query-test.ts b/packages/core/query-test.ts new file mode 100644 index 00000000..47ca3c4e --- /dev/null +++ b/packages/core/query-test.ts @@ -0,0 +1,74 @@ +import { describe, test } from '@std/testing/bdd' +import { expect } from '@std/expect' +import { QueryBuilder } from './query.ts' +import { DomainProjectionSelector } from '@iroha/core/data-model' +import * as types from './data-model/mod.ts' +import type { PartialDeep } from 'type-fest' + +describe('QueryBuilder', () => { + test('default selector is [atom]', () => { + const builder = new QueryBuilder('FindDomains') + + expect(builder.build().query.value.selector).toEqual([{ kind: 'Atom' }]) + }) + + test('when single selector is passed, it is wrapped into an array', () => { + const builder = new QueryBuilder('FindDomains').selectWith((domain) => domain.id) + + expect(builder.build().query.value.selector).toEqual([DomainProjectionSelector.Id.Atom]) + }) + + test('when array selector is passed, it is preserved as-is', () => { + const builder: QueryBuilder<'FindAccounts', [types.AccountId, types.Metadata, types.PublicKey, types.Json]> = + new QueryBuilder('FindAccounts') + .selectWith((proto) => [ + proto.id, + proto.metadata, + proto.id.signatory, + proto.metadata.key(new types.Name('test')), + ]) + + expect(builder.build().query.value.selector).toEqual([ + types.AccountProjectionSelector.Id.Atom, + types.AccountProjectionSelector.Metadata.Atom, + types.AccountProjectionSelector.Id.Signatory.Atom, + types.AccountProjectionSelector.Metadata.Key({ key: new types.Name('test'), projection: { kind: 'Atom' } }), + ]) + }) + + test('uses PASS predicate by default (orphan "and")', () => { + const builder = new QueryBuilder('FindAssets') + + expect(builder.build().query.value.predicate).toEqual({ kind: 'And', value: [] }) + }) + + test('accepts query with payload', () => { + const payload = { + assetDefinition: types.AssetDefinitionId.parse('test#time'), + } as const + + const builder = new QueryBuilder('FindAccountsWithAsset', payload) + + expect(builder.build().query.value.query).toEqual(payload) + }) + + test('accepts query with payload and params', () => { + const payload = { + assetDefinition: types.AssetDefinitionId.parse('test#time'), + } as const + + const builder = new QueryBuilder('FindAccountsWithAsset', payload, { fetchSize: new types.NonZero(5) }) + + expect(builder.build().query.value.query).toEqual(payload) + expect(builder.build().params).toMatchObject({ fetchSize: new types.NonZero(5n) }) + }) + + test('accepts query without payload but with params', () => { + const builder = new QueryBuilder('FindTransactions', { offset: 2, limit: new types.NonZero(5) }) + + expect(builder.build().query.value.query).toEqual(null) + expect(builder.build().params).toMatchObject( + { pagination: { offset: 2n, limit: new types.NonZero(5n) } } satisfies PartialDeep, + ) + }) +}) diff --git a/packages/core/query-types.test-d.ts b/packages/core/query-types.test-d.ts new file mode 100644 index 00000000..d6af3dfd --- /dev/null +++ b/packages/core/query-types.test-d.ts @@ -0,0 +1,81 @@ +// deno-lint-ignore-file no-unused-vars + +import { CompoundPredicate } from './data-model/compound.ts' +import * as types from './data-model/mod.ts' +import type * as proto from './data-model/prototypes.generated.ts' +import type { QUERIES_WITH_PAYLOAD } from './query-internal.ts' +import { type GetSingularQueryOutput, QueryBuilder, type QueryKind, type SingularQueryOutputMap } from './query.ts' + +type Expect = T +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false + +type BuilderOutput> = Q extends QueryBuilder ? O : never + +type test_all_queries_in_predicates = Expect> +type test_all_queries_in_selectors = Expect> +type test_all_queries_in_compat = Expect> + +const selectAccountDomainName = new QueryBuilder('FindAccounts').selectWith((acc) => acc.id.domain.name) +type test_selector_account_domain_name = Expect< + Equal< + BuilderOutput, + types.Name + > +> + +const selectMetadataKey = new QueryBuilder('FindDomains').selectWith((x) => x.metadata.key(new types.Name('test'))) +type test_selector_metadata_key = Expect< + Equal< + BuilderOutput, + types.Json + > +> + +const selectAccountAndId = new QueryBuilder('FindAccounts').selectWith((x) => [x, x.id]) +type test_find_account_and_id = Expect< + Equal< + BuilderOutput, + [types.Account, types.AccountId] + > +> + +const selectBlocksDefault = new QueryBuilder('FindBlocks') +type test_default_output_for_blocks = Expect, types.SignedBlock>> + +type singular_queries_in_output = keyof SingularQueryOutputMap +type singular_query_output_kinds = types.SingularQueryOutputBox['kind'] + +type test_singular_query_output_map_covers_all_queries = Expect< + Equal +> + +type test_singular_query_output_map_matches_with_output_box = Expect< + Equal +> + +type test_singular_query_output_dm = Expect< + Equal, types.ExecutorDataModel> +> +type test_singular_query_output_params = Expect, types.Parameters>> + +const filterAllKinds = new QueryBuilder('FindAssets').filterWith((asset) => + CompoundPredicate.Or( + CompoundPredicate.Not( + CompoundPredicate.And( + CompoundPredicate.Atom(asset.id.account.domain.name.endsWith('test')), + CompoundPredicate.Atom(asset.id.definition.name.contains('test')), + ), + ), + CompoundPredicate.Atom(asset.value.isStore()), + CompoundPredicate.Atom(asset.value.store.key(new types.Name('test')).equals(types.Json.fromValue([false, true]))), + ) +) + +type TrueQueriesWithPayload = { + [K in QueryKind]: (types.QueryBox & { kind: K })['value'] extends types.QueryWithFilter ? never + : K +}[QueryKind] + +type HardcodedQueriesWithPayload = typeof QUERIES_WITH_PAYLOAD extends Set ? T : never + +type test_queries_with_payload = Expect> diff --git a/packages/core/query-types.ts b/packages/core/query-types.ts new file mode 100644 index 00000000..416d21a8 --- /dev/null +++ b/packages/core/query-types.ts @@ -0,0 +1,56 @@ +import type * as types from './data-model/mod.ts' +import type * as prototypes from './data-model/prototypes.generated.ts' + +/** + * Type map, defining relation between the variants of {@link types.SingularQueryBox} and its outputs in + * {@link types.SingularQueryOutputBox} + */ +export interface SingularQueryOutputMap { + FindExecutorDataModel: 'ExecutorDataModel' + FindParameters: 'Parameters' +} + +/** + * Kinds of singular queries. + */ +export type SingularQueryKind = keyof SingularQueryOutputMap & types.SingularQueryBox['kind'] + +/** + * Type function to map a singular query kind into the value of its output box. + */ +export type GetSingularQueryOutput = SingularQueryOutputMap[K] extends + infer OutputKind extends types.SingularQueryOutputBox['kind'] + ? (types.SingularQueryOutputBox & { kind: OutputKind })['value'] + : never + +/** + * Kinds of _iterable_ queries. + * + * Note that this differs from {@link SingularQueryKind}. + */ +export type QueryKind = + & types.QueryBox['kind'] + & keyof prototypes.QueryPredicates + & keyof prototypes.QuerySelectors + & keyof prototypes.QueryCompatibleSelectors + +/** + * Maps query kind to its corresponding predicate type. + */ +export type PredicateOf = (types.QueryBox & { kind: Q })['value'] extends + types.QueryWithFilter, any> ? P + : never + +type MapSelectedTuple = O extends + readonly [{ __selector: infer Id extends keyof prototypes.SelectorIdToOutput }, ...infer Rest] + ? [prototypes.SelectorIdToOutput[Id], ...MapSelectedTuple] + : O extends [] ? [] + : never + +export type SelectedTuple = O extends { __selector: infer Id extends keyof prototypes.SelectorIdToOutput } + ? prototypes.SelectorIdToOutput[Id] + : O extends readonly [infer Single] ? SelectedTuple + : MapSelectedTuple + +export type GetQueryPayload = (types.QueryBox & { kind: K })['value'] extends + types.QueryWithFilter ? Payload : never diff --git a/packages/core/query.spec-d.ts b/packages/core/query.spec-d.ts deleted file mode 100644 index 4f8b5b68..00000000 --- a/packages/core/query.spec-d.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { - Account, - AccountId, - Asset, - ExecutorDataModel, - Json, - Metadata, - Name, - Parameters, - QueryBox, - QueryOutputBatchBox, - QuerySelectorMap, - SelectorOutputMap, - SignedBlock, - SingularQueryBox, - SingularQueryOutputBox, -} from '@iroha/core/data-model' -import type { - DefaultQueryOutput, - GetQueryOutput, - GetSelectorOutput, - GetSingularQueryOutput, - SelectorToOutput, - SingularQueryOutputMap, -} from './query.ts' -import type { Variant, VariantUnit } from './util.ts' - -type Expect = T -type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false - -type test_query_selector_map_has_all_queries = Expect> - -type dbg1 = QuerySelectorMap[keyof QuerySelectorMap] -type dbg2 = keyof SelectorOutputMap - -type test_query_selector_map_matches_selector_output_map = Expect< - QuerySelectorMap[keyof QuerySelectorMap] extends keyof SelectorOutputMap ? true : false -> - -type selector_output_atoms = SelectorOutputMap[keyof SelectorOutputMap]['Atom'] -type query_output_box_kinds = QueryOutputBatchBox['kind'] - -type test_all_selector_output_atoms_are_present_in_output_box = Expect< - Equal, never> -> - -/** - * Effectively these output kinds are impossible to receive from Iroha at the moment. - * - * The `String` output exists because there is a `StringPredicateAtom`. It is a design - * limitation (or implementation issue?) of selectors & predicates in Iroha codebase - - * they mirror each other. - * - * As for `Parameter`, it is neither possible to use in predicates nor in selectors and is - * completely redundant at the moment. Maybe it was added as a stub for future. - */ -type test_few_output_options_arent_covered_by_selectors = Expect< - Equal, 'String' | 'Parameter'> -> - -type test_selector_account_domain_name = Expect< - Equal< - GetSelectorOutput<'Account', Variant<'Id', Variant<'Domain', Variant<'Name', VariantUnit<'Atom'>>>>>, - // - Name - > -> - -type test_selector_metadata_key = Expect< - Equal< - GetSelectorOutput<'Metadata', Variant<'Key', { key: Name; projection: VariantUnit<'Atom'> }>>, - // - Json - > -> - -type test_selector_accout_metadata_key = Expect< - Equal< - GetSelectorOutput<'Account', Variant<'Metadata', Variant<'Key', { key: Name; projection: VariantUnit<'Atom'> }>>>, - // - Json - > -> - -type test_selector_metadata_atom = Expect>, Metadata>> - -type test_find_account_and_id = Expect< - Equal< - SelectorToOutput<'FindAccounts', [VariantUnit<'Atom'>, Variant<'Id', VariantUnit<'Atom'>>]>, - [Account, AccountId] - > -> - -type test_find_account_metadata_key = Expect< - Equal< - SelectorToOutput< - 'FindAccounts', - Variant<'Metadata', Variant<'Key', { key: Name; projection: VariantUnit<'Atom'> }>> - >, - Json - > -> - -type test_default_output_for_blocks = Expect, SignedBlock>> - -type singular_queries_in_output = keyof SingularQueryOutputMap -type singular_query_output_kinds = SingularQueryOutputBox['kind'] - -type test_singular_query_output_map_covers_all_queries = Expect< - Equal -> - -type test_singular_query_output_map_matches_with_output_box = Expect< - Equal -> - -type test_singular_query_output_dm = Expect, ExecutorDataModel>> -type test_singular_query_output_params = Expect, Parameters>> - -type test_query_outputs = - & Expect>, Account>> - & Expect, Account>> - -type test_selector_as_array_not_tuple = Expect[]>, Asset[]>> diff --git a/packages/core/query.spec.ts b/packages/core/query.spec.ts deleted file mode 100644 index 88c7691d..00000000 --- a/packages/core/query.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { buildQuery } from './query.ts' -import { DomainProjectionSelector } from '@iroha/core/data-model' - -describe('buildQuery()', () => { - test('default selector is [atom]', () => { - const res = buildQuery('FindDomains', null) - - expect(res.query.query.value.selector).toEqual([{ kind: 'Atom' }]) - }) - - test('when single selector is passed, it is wrapped into an array', () => { - const res = buildQuery('FindDomains', null, { selector: DomainProjectionSelector.Id.Atom }) - - expect(res.query.query.value.selector).toEqual([DomainProjectionSelector.Id.Atom]) - }) - - test('when array selector is passed, it is preserved as-is', () => { - const SELECTOR = [DomainProjectionSelector.Id.Name.Atom, DomainProjectionSelector.Metadata.Atom] - - const res = buildQuery('FindDomains', null, { selector: SELECTOR }) - - expect(res.query.query.value.selector).toEqual(SELECTOR) - }) - - test('uses PASS predicate by default (orphan "and")', () => { - const res = buildQuery('FindAssets', null) - - expect(res.query.query.value.predicate).toEqual({ kind: 'And', value: [] }) - }) -}) diff --git a/packages/core/query.ts b/packages/core/query.ts index 222ee082..d6b232fd 100644 --- a/packages/core/query.ts +++ b/packages/core/query.ts @@ -1,155 +1,147 @@ +import { fail } from '@std/assert/fail' import * as types from './data-model/mod.ts' -import type { VariantUnit } from './util.ts' +import type * as prototypes from './data-model/prototypes.generated.ts' +import { getActualSelector, predicateProto, QUERIES_WITH_PAYLOAD, selectorProto } from './query-internal.ts' +import type { GetQueryPayload, PredicateOf, QueryKind, SelectedTuple } from './query-types.ts' -/** - * Type map, defining relation between the variants of {@link types.SingularQueryBox} and its outputs in - * {@link types.SingularQueryOutputBox} - */ -export interface SingularQueryOutputMap { - FindExecutorDataModel: 'ExecutorDataModel' - FindParameters: 'Parameters' -} +export * from './query-types.ts' -/** - * Kinds of singular queries. - */ -export type SingularQueryKind = keyof SingularQueryOutputMap & types.SingularQueryBox['kind'] +const DEFAULT_SELECTOR = [{ kind: 'Atom' }] -/** - * Type function to map a singular query kind into the value of its output box. - */ -export type GetSingularQueryOutput = SingularQueryOutputMap[K] extends - infer OutputKind extends types.SingularQueryOutputBox['kind'] - ? (types.SingularQueryOutputBox & { kind: OutputKind })['value'] - : never +function buildQueryParams( + params?: QueryBuilderParams, +): types.QueryParams { + return { + fetchSize: params?.fetchSize?.map(BigInt) ?? null, + pagination: { + offset: params?.offset ? BigInt(params.offset) : 0n, + limit: params?.limit?.map(BigInt) ?? null, + }, + sorting: { + sortByMetadataKey: params?.sorting?.byMetadataKey ?? null, + }, + } +} -/** - * Type function to map the selector of arbitrary shape to the corresponding output value. - */ -export type GetSelectorOutput = S extends { kind: 'Atom' } - ? types.SelectorOutputMap[K]['Atom'] extends infer OutputKind extends types.QueryOutputBatchBox['kind'] - ? (types.QueryOutputBatchBox & { kind: OutputKind })['value'] extends (infer Output)[] ? Output - : never +export type QueryBuilderCtorArgs = GetQueryPayload extends infer P + ? P extends null ? [query: Q, params?: QueryBuilderParams] : [query: Q, payload: P, params?: QueryBuilderParams] : never - : [K, S] extends ['Metadata', { kind: 'Key'; value: { key: types.Name; projection: infer NextS } }] - ? GetSelectorOutput<'Json', NextS> - : S extends { kind: infer SKind extends keyof types.SelectorOutputMap[K]; value: infer NextS } - ? types.SelectorOutputMap[K][SKind] extends infer NextK extends keyof types.SelectorOutputMap - ? GetSelectorOutput - : never - : never - -/** - * Kinds of _iterable_ queries. - * - * Note that this differs from {@link SingularQueryKind}. - */ -export type QueryKind = types.QueryBox['kind'] & keyof types.QuerySelectorMap -/** - * Map a tuple with {@link GetSelectorOutput}. - */ -export type GetSelectorTupleOutput = Tuple extends [ - infer Head, - ...infer Tail, -] ? [GetSelectorOutput, ...GetSelectorTupleOutput] - : Tuple extends [] ? [] - // Not as ergonomic (lost `const` somewhere?), but still works and is correct - : Tuple extends Array ? GetSelectorOutput[] - : never +export type DefaultQueryOutput = SelectedTuple /** - * Maps a query kind and a selector to the corresponding output of this query and this selector. + * Utility to build queries in a safe and convenient way. * - * The selector must be either a single value or an array. Values of the selectors are actual variants - * and structs of the selectors in the schema. - * - * If the selector is a single value or an array with a single value, the output will be just a value. + * It is a lower-level utility that only builds queries. For the higher-level + * implementation which actually sends the queries, see `QueryBuilder` from `@iroha/client` package. */ -export type SelectorToOutput< - Q extends QueryKind & keyof types.QuerySelectorMap, - Selection, -> = types.QuerySelectorMap[Q] extends infer Selector extends keyof types.SelectorOutputMap - ? Selection extends readonly [infer S] ? GetSelectorOutput - : Selection extends readonly [] ? never - : Selection extends readonly [...infer S] ? GetSelectorTupleOutput - : GetSelectorOutput - : never - -/** - * The default output is the root value of the query output, without any projections inside. - */ -export type DefaultQueryOutput = GetSelectorOutput< - Q extends keyof types.QuerySelectorMap - ? types.QuerySelectorMap[Q] extends infer Selector extends keyof types.SelectorOutputMap ? Selector - : never - : never, - VariantUnit<'Atom'> -> +export class QueryBuilder> { + protected kind: Q + protected payload: GetQueryPayload + protected params: types.QueryParams + protected selector: unknown = DEFAULT_SELECTOR + protected predicate: types.CompoundPredicate = types.CompoundPredicate.PASS + protected parseOutput: (resp: types.QueryOutputBatchBoxTuple) => Generator = generateOutputTuples + + public constructor(...args: QueryBuilderCtorArgs) { + this.kind = args[0] + + if ((QUERIES_WITH_PAYLOAD as Set).has(this.kind)) { + ;[this.payload, this.params] = args.length === 2 + ? [args[1] as GetQueryPayload, buildQueryParams()] + : args.length === 3 + ? [args[1] as GetQueryPayload, buildQueryParams(args[2])] + : fail('bad arguments') + } else { + ;[this.payload, this.params] = ( + // @ts-expect-error it doesn't understand that there is an optional param + args.length === 1 + ) + ? [null as GetQueryPayload, buildQueryParams()] + : args.length === 2 + ? [null as GetQueryPayload, buildQueryParams(args[1] as QueryBuilderParams)] + : fail('bad arguments') + } + } -const DEFAULT_SELECTOR = [{ kind: 'Atom' }] + /** + * Specify selected tuple with a _prototype_ of the querying object. + * + * @example Select a single value + * + * ```ts + * import * as types from '@iroha/core/data-model' + * + * const builder: QueryBuilder<'FindAccounts', types.DomainId> = + * new QueryBuilder('FindAccounts') + * .selectWith((account) => account.id.domain) + * ``` + * + * @example Select multiple values + * + * ```ts + * import * as types from '@iroha/core/data-model' + * + * const builder: QueryBuilder<'FindTransactions', [types.Hash, null | types.TransactionRejectionReason]> = + * new QueryBuilder('FindTransactions') + * .selectWith((tx) => [tx.value.hash, tx.error]) + * ``` + */ + public selectWith( + fn: (prototype: prototypes.QuerySelectors[Q]) => ProtoTuple, + ): QueryBuilder> { + const proto = selectorProto() + const protoTuple = fn(proto) + this.selector = protoTupleIntoActualSelector(protoTuple) + return this as QueryBuilder> + } -/** - * Maps query kind to its corresponding predicate type. - */ -export type PredicateFor = (types.QueryBox & { kind: Q })['value'] extends - types.QueryWithFilter, any> ? P - : never + /** + * Specify predicate with a _prototype_ of the querying object. + * + * The returned type must be a variant {@linkcode [data-model].CompoundPredicate | CompoundPredicate} containing + * the actual predicate. + * + * @example Specifying a compound logical predicate + * + * ```ts + * import * as types from '@iroha/core/data-model' + * + * new QueryBuilder('FindAccounts') + * .filterWith((account) => types.CompoundPredicate.Or( + * types.CompoundPredicate.Atom(account.id.domain.name.startsWith('wonder')), + * types.CompoundPredicate.Atom(account.id.domain.name.endsWith('land')), + * )) + * ``` + */ + public filterWith(fn: (prototype: prototypes.QueryPredicates[Q]) => types.CompoundPredicate>): this { + const proto = predicateProto() + this.predicate = fn(proto) + return this + } -/** - * Utility to build a query in a type-safe way. - * - * @param kind kind of the query - * @param payload payload of the query (for most, it's `null`) - * @param params params such as predicate, selector, pagination etc - * @returns a constructed {@link types.QueryWithParams} and a function that extracts the typed output from - * query response. - */ -export function buildQuery>( - kind: K, - payload: GetQueryPayload, - params?: P, -): BuildQueryResult> { - return { - query: { + public build(): types.QueryWithParams { + return { query: { - kind, + kind: this.kind, value: { - query: payload, - predicate: params?.predicate || types.CompoundPredicate.PASS, - selector: params?.selector - ? Array.isArray(params?.selector) ? params.selector : [params.selector] - : DEFAULT_SELECTOR, - } as types.QueryWithFilter, unknown>, + query: this.payload, + predicate: this.predicate, + selector: this.selector, + } satisfies types.QueryWithFilter, } as types.QueryBox, - params: { - fetchSize: params?.fetchSize?.map(BigInt) ?? null, - pagination: { - offset: params?.offset ? BigInt(params.offset) : 0n, - limit: params?.limit?.map(BigInt) ?? null, - }, - sorting: { - sortByMetadataKey: params?.sorting?.byMetadataKey ?? null, - }, - }, - }, - parseResponse: generateOutputTuples, + params: this.params, + } } } -// TODO: document fields! -/** - * Params for {@link buildQuery}. - */ -export interface BuildQueryParams { - /** - * TODO how to work with predicates - */ - predicate?: types.CompoundPredicate> - /** - * TODO examples of selectors - */ - selector?: unknown +function protoTupleIntoActualSelector(tuple: unknown): unknown { + if (Array.isArray(tuple)) { + return tuple.map((x) => getActualSelector(x)) + } + return [getActualSelector(tuple)] +} +export interface QueryBuilderParams { /** * TODO what fetch size affects, why to set */ @@ -174,25 +166,6 @@ export interface BuildQueryParams { } } -export type GetQueryPayload = (types.QueryBox & { kind: K })['value'] extends - types.QueryWithFilter ? Payload : never - -/** - * Result of {@link buildQuery} - */ -export interface BuildQueryResult { - query: types.QueryWithParams - parseResponse: (response: types.QueryOutputBatchBoxTuple) => Generator -} - -/** - * Utility type - */ -export type GetQueryOutput> = P extends { - selector: infer S -} ? SelectorToOutput - : SelectorToOutput> - function* generateOutputTuples(response: types.QueryOutputBatchBoxTuple): Generator { // FIXME: this is redundant in runtime, just a safe guard // invariant( diff --git a/tests/browser/cypress/tsconfig.json b/tests/browser/cypress/tsconfig.json deleted file mode 100644 index 05389ee2..00000000 --- a/tests/browser/cypress/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["cypress"], - "allowJs": true - }, - "include": ["./**/*.js", "./**/*.ts"] -} diff --git a/tests/node/tests/client-misc.spec.ts b/tests/node/tests/client-misc.spec.ts index 86c9901f..9afff447 100644 --- a/tests/node/tests/client-misc.spec.ts +++ b/tests/node/tests/client-misc.spec.ts @@ -130,9 +130,8 @@ describe('Queries', () => { await expect( client.find - .assets({ - predicate: dm.CompoundPredicate.FAIL, - }) + .assets() + .filterWith(() => dm.CompoundPredicate.FAIL) .executeSingle(), ).rejects.toThrowErrorMatchingInlineSnapshot(`[TypeError: Expected query to return exactly one element, got 0]`) }) @@ -142,18 +141,18 @@ describe('Queries', () => { const { bob } = await submitTestData(client) const items = await client.find - .accounts({ - predicate: dm.CompoundPredicate.Or( - dm.CompoundPredicate.Atom(dm.AccountProjectionPredicate.Id.Domain.Name.Atom.Contains('cert')), + .accounts() + .filterWith((account) => + dm.CompoundPredicate.Or( dm.CompoundPredicate.Atom( - dm.AccountProjectionPredicate.Id.Signatory.Atom.Equals(bob.publicKey()), + account.id.domain.name.contains('cert'), ), - ), - selector: dm.AccountProjectionSelector.Metadata.Key({ - key: new dm.Name('alias'), - projection: { kind: 'Atom' }, - }), - }) + dm.CompoundPredicate.Atom( + account.id.signatory.equals(bob.publicKey()), + ), + ) + ) + .selectWith((account) => account.metadata.key(new dm.Name('alias'))) .executeAll() expect(items.map((x) => x.asValue())).toEqual(['Bob', 'Mad Hatter']) @@ -168,13 +167,7 @@ describe('Queries', () => { { assetDefinition: dm.AssetDefinitionId.parse('neko_coin#wherever'), }, - { - selector: dm.AccountProjectionSelector.Metadata.Key({ - key: new dm.Name('alias'), - projection: { kind: 'Atom' }, - }), - }, - ) + ).selectWith((acc) => acc.metadata.key(new dm.Name('alias'))) .executeAll() expect(items.map((x) => x.asValue())).toEqual(['Bob']) @@ -184,8 +177,8 @@ describe('Queries', () => { const { client } = await usePeer() await submitTestData(client) - const blocks = await client.find.blocks({ selector: dm.SignedBlockProjectionSelector.Header.Atom }).executeAll() - const headers = await client.find.blockHeaders().executeAll() + const blocks: dm.BlockHeader[] = await client.find.blocks().selectWith((block) => block.header).executeAll() + const headers: dm.BlockHeader[] = await client.find.blockHeaders().executeAll() expect(blocks).toEqual(headers) }) @@ -203,8 +196,8 @@ describe('Queries', () => { const { client } = await usePeer() await submitTestData(client) - const all = await client.find.triggers({ selector: dm.TriggerProjectionSelector.Id.Atom }).executeAll() - const activeOnes = await client.find.activeTriggerIds().executeAll() + const all: dm.Name[] = await client.find.triggers().selectWith((trigger) => trigger.id).executeAll() + const activeOnes: dm.Name[] = await client.find.activeTriggerIds().executeAll() expect(all).toEqual(activeOnes) expect(activeOnes).toMatchInlineSnapshot(` @@ -219,11 +212,10 @@ describe('Queries', () => { const { client } = await usePeer() await submitTestData(client) - const items = await client.find - .assets({ - predicate: dm.CompoundPredicate.Atom(dm.AssetProjectionPredicate.Id.Definition.Name.Atom.EndsWith('_coin')), - selector: dm.AssetProjectionSelector.Id.Definition.Name.Atom, - }) + const items: dm.Name[] = await client.find + .assets() + .filterWith((asset) => dm.CompoundPredicate.Atom(asset.id.definition.name.endsWith('_coin'))) + .selectWith((asset) => asset.id.definition.name) .executeAll() expect(items.map((x) => x.value)).contain.all.members(['base_coin', 'neko_coin', 'gator_coin']) @@ -236,10 +228,10 @@ describe('Queries', () => { const domains = await client.find.domains().executeAll() expect(domains.length).toBeGreaterThan(0) - const failed = await client.find.domains({ predicate: dm.CompoundPredicate.FAIL }).executeAll() + const failed = await client.find.domains().filterWith(() => dm.CompoundPredicate.FAIL).executeAll() expect(failed).toHaveLength(0) - const passed = await client.find.domains({ predicate: dm.CompoundPredicate.PASS }).executeAll() + const passed = await client.find.domains().filterWith(() => dm.CompoundPredicate.PASS).executeAll() expect(passed).toEqual(domains) }) @@ -247,12 +239,12 @@ describe('Queries', () => { const { client } = await usePeer() await submitTestData(client) - const ids = await client.find + const ids: dm.Name[] = await client.find .domains({ offset: 5, limit: new dm.NonZero(3), - selector: dm.DomainProjectionSelector.Id.Name.Atom, }) + .selectWith((x) => x.id.name) .executeAll() expect(ids).toMatchInlineSnapshot(` @@ -271,9 +263,9 @@ describe('Queries', () => { const stream = client.find .domains({ fetchSize: new dm.NonZero(5), - selector: dm.DomainProjectionSelector.Id.Name.Atom, }) - .batches() + .selectWith((x) => x.id.name) + .batches() satisfies AsyncGenerator // TODO: include information about remaining items into the stream return value let batch = await stream.next() @@ -339,13 +331,13 @@ describe('Queries', () => { const someBlock = (await client.find.blocks().executeAll()).at(1)! const found = await client.find - .blocks({ - predicate: dm.CompoundPredicate.Atom( - dm.SignedBlockProjectionPredicate.Header.Hash.Atom.Equals( + .blocks().filterWith((block) => + dm.CompoundPredicate.Atom( + block.header.hash.equals( blockHash(someBlock.value.payload.header), ), - ), - }) + ) + ) .executeSingle() expect(found.value.payload.header).toEqual(someBlock.value.payload.header) @@ -365,12 +357,13 @@ describe('Queries', () => { await submitTestData(client) const assets = await client.find - .assets({ - predicate: dm.CompoundPredicate.Atom( - dm.AssetProjectionPredicate.Id.Definition.Atom.Equals(dm.AssetDefinitionId.parse('gator_coin#certainty')), - ), - selector: [dm.AssetProjectionSelector.Id.Account.Atom, dm.AssetProjectionSelector.Value.Numeric.Atom], - }) + .assets() + .filterWith((asset) => + dm.CompoundPredicate.Atom( + asset.id.definition.equals(dm.AssetDefinitionId.parse('gator_coin#certainty')), + ) + ) + .selectWith((asset) => [asset.id.account, asset.value.numeric]) .executeAll() expect(assets).toMatchInlineSnapshot(` @@ -399,10 +392,9 @@ describe('Queries', () => { await expect( client.find - .assets({ - predicate: dm.CompoundPredicate.Atom(dm.AssetProjectionPredicate.Value.Atom.IsNumeric), - selector: [dm.AssetProjectionSelector.Value.Store.Atom], - }) + .assets() + .filterWith((asset) => dm.CompoundPredicate.Atom(asset.value.isNumeric())) + .selectWith((asset) => asset.value.store) .executeAll(), ).rejects.toEqual( new QueryValidationError(dm.ValidationFail.QueryFailed.Conversion('Expected store value, got numeric')), @@ -511,9 +503,8 @@ describe('Transactions', () => { const hash = tx.hash const _found = await client.find - .transactions({ - predicate: dm.CompoundPredicate.Atom(dm.CommittedTransactionProjectionPredicate.Value.Hash.Atom.Equals(hash)), - }) + .transactions() + .filterWith((tx) => dm.CompoundPredicate.Atom(tx.value.hash.equals(hash))) .executeSingle() // TODO: