diff --git a/CHANGELOG b/CHANGELOG index 4c15c9b11..25fbec1de 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ This project _loosely_ adheres to [Semantic Versioning](https://semver.org/spec/ ### Changed - Partially migrated the Runtime service to Rust. (DM language installation for agents is pending.) [PR#466](https://github.com/coasys/ad4m/pull/466) +- Improved performance of SDNA / SubjectClass functions by moving code from client into executor and saving a lot of client <-> executor roundtrips [PR#480](https://github.com/coasys/ad4m/pull/480) ## [0.9.0] - 23/03/2024 diff --git a/Cargo.lock b/Cargo.lock index dd700c28a..af58b521f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,7 @@ dependencies = [ "holochain_types", "include_dir", "itertools 0.10.5", + "json5", "jsonwebtoken", "kitsune_p2p_types", "lazy_static", @@ -7018,6 +7019,17 @@ dependencies = [ "treediff 4.0.2", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -9296,15 +9308,49 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.6" +version = "2.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" dependencies = [ "memchr", "thiserror", "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 2.0.48", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + [[package]] name = "petgraph" version = "0.6.4" diff --git a/ad4m-hooks/helpers/src/factory/SubjectRepository.ts b/ad4m-hooks/helpers/src/factory/SubjectRepository.ts index b432ca9e7..51f0eb6cb 100644 --- a/ad4m-hooks/helpers/src/factory/SubjectRepository.ts +++ b/ad4m-hooks/helpers/src/factory/SubjectRepository.ts @@ -73,8 +73,6 @@ export class SubjectRepository { } async update(id: string, data: QueryPartialEntity) { - await this.ensureSubject(); - const instance = await this.get(id); if (!instance) { @@ -102,8 +100,8 @@ export class SubjectRepository { } async get(id: string): Promise { - await this.ensureSubject(); if (id) { + await this.ensureSubject(); const subjectProxy = await this.perspective.getSubjectProxy( id, this.subject @@ -118,7 +116,6 @@ export class SubjectRepository { } async getData(id: string): Promise { - await this.ensureSubject(); const entry = await this.get(id); if (entry) { // @ts-ignore @@ -133,28 +130,21 @@ export class SubjectRepository { new LinkQuery({ source: entry.baseExpression }) ); - const getters = Object.entries(Object.getOwnPropertyDescriptors(entry)) - .filter(([key, descriptor]) => typeof descriptor.get === "function") - .map(([key]) => key); - - const promises = getters.map((getter) => entry[getter]); - return Promise.all(promises).then((values) => { - return getters.reduce((acc, getter, index) => { - let value = values[index]; - if (this.tempSubject.prototype?.__properties[getter]?.transform) { - value = - this.tempSubject.prototype.__properties[getter].transform(value); - } + let data: any = await this.perspective.getSubjectData(this.subject, entry.baseExpression) - return { - ...acc, - id: entry.baseExpression, - timestamp: links[0].timestamp, - author: links[0].author, - [getter]: value, - }; - }, {}); - }); + for (const key in data) { + if (this.tempSubject.prototype?.__properties[key]?.transform) { + data[key] = + this.tempSubject.prototype.__properties[key].transform(data[key]); + } + } + + return { + id: entry.baseExpression, + timestamp: links[0].timestamp, + author: links[0].author, + ...data, + } } async getAll(source?: string, query?: QueryOptions): Promise { @@ -232,8 +222,6 @@ export class SubjectRepository { source?: string, query?: QueryOptions ): Promise { - await this.ensureSubject(); - const subjects = await this.getAll(source, query); const entries = await Promise.all( diff --git a/core/src/Ad4mClient.test.ts b/core/src/Ad4mClient.test.ts index 8c27546ab..3c5e15733 100644 --- a/core/src/Ad4mClient.test.ts +++ b/core/src/Ad4mClient.test.ts @@ -34,9 +34,9 @@ jest.setTimeout(15000) async function createGqlServer(port: number) { const schema = await buildSchema({ resolvers: [ - AgentResolver, + AgentResolver, ExpressionResolver, - LanguageResolver, + LanguageResolver, NeighbourhoodResolver, PerspectiveResolver, RuntimeResolver @@ -87,7 +87,7 @@ async function createGqlServer(port: number) { describe('Ad4mClient', () => { let ad4mClient let apolloClient - + beforeAll(async () => { let port = await createGqlServer(4000); @@ -98,7 +98,7 @@ describe('Ad4mClient', () => { webSocketImpl: Websocket })); - + apolloClient = new ApolloClient({ link: wsLink, @@ -254,9 +254,9 @@ describe('Ad4mClient', () => { appDesc: "demo-desc", appDomain: "demo.test.org", appUrl: "https://demo-link", - appIconPath: "/some/image/path", + appIconPath: "/some/image/path", capabilities: [ - { + { with: { "domain":"agent", "pointers":["*"] @@ -267,7 +267,7 @@ describe('Ad4mClient', () => { expect(requestId).toBe("test-request-id") }) - + it('agentGetApps() smoke tests', async () => { const apps = await ad4mClient.agent.getApps() expect(apps.length).toBe(0) @@ -705,7 +705,7 @@ describe('Ad4mClient', () => { it('addListener() smoke test', async () => { let perspective = await ad4mClient.perspective.byUUID('00004') - + const testLink = new LinkExpression() testLink.author = "did:ad4m:test" testLink.timestamp = Date.now().toString() @@ -726,7 +726,7 @@ describe('Ad4mClient', () => { const link = new LinkExpressionInput() link.source = 'root' link.target = 'perspective://Qm34589a3ccc0' - await perspective.add(link) + await perspective.add(link) expect(linkAdded).toBeCalledTimes(1) expect(linkRemoved).toBeCalledTimes(0) @@ -734,7 +734,7 @@ describe('Ad4mClient', () => { perspective = await ad4mClient.perspective.byUUID('00004') await perspective.addListener('link-removed', linkRemoved) - await perspective.remove(testLink) + await perspective.remove(testLink) expect(linkAdded).toBeCalledTimes(1) expect(linkRemoved).toBeCalledTimes(1) @@ -742,20 +742,20 @@ describe('Ad4mClient', () => { it('removeListener() smoke test', async () => { let perspective = await ad4mClient.perspective.byUUID('00004') - + const linkAdded = jest.fn() await perspective.addListener('link-added', linkAdded) - await perspective.add({source: 'root', target: 'neighbourhood://Qm12345'}) + await perspective.add({source: 'root', target: 'neighbourhood://Qm12345'}) expect(linkAdded).toBeCalledTimes(1) linkAdded.mockClear(); - + perspective = await ad4mClient.perspective.byUUID('00004') await perspective.removeListener('link-added', linkAdded) - await perspective.add({source: 'root', target: 'neighbourhood://Qm123456'}) + await perspective.add({source: 'root', target: 'neighbourhood://Qm123456'}) expect(linkAdded).toBeCalledTimes(1) }) @@ -774,7 +774,7 @@ describe('Ad4mClient', () => { it('updateLink() smoke test', async () => { const link = await ad4mClient.perspective.updateLink( - '00001', + '00001', {author: '', timestamp: '', proof: {signature: '', key: ''}, data:{source: 'root', target: 'none'}}, {source: 'root', target: 'lang://Qm123', predicate: 'p'}) expect(link.author).toBe('did:ad4m:test') @@ -792,6 +792,30 @@ describe('Ad4mClient', () => { const r = await ad4mClient.perspective.addSdna('00001', "Test", 'subject_class("Test", test)', 'subject_class'); expect(r).toBeTruthy() }) + + it('executeCommands() smoke test', async () => { + const result = await ad4mClient.perspective.executeCommands( + '00001', + 'command1; command2', + 'expression1', + 'param1, param2' + ); + expect(result).toBeTruthy(); + }) + + it('getSubjectData() smoke test', async () => { + const result = await ad4mClient.perspective.getSubjectData('00001', 'Test', 'test'); + expect(result).toBe(""); + }); + + it('createSubject() smoke test', async () => { + const result = await ad4mClient.perspective.createSubject( + '00001', + 'command1; command2', + 'expression1', + ); + expect(result).toBeTruthy(); + }) }) describe('.runtime', () => { @@ -804,7 +828,7 @@ describe('Ad4mClient', () => { const r = await ad4mClient.runtime.openLink('https://ad4m.dev') expect(r).toBeTruthy() }) - + it('addTrustedAgents() smoke test', async () => { const r = await ad4mClient.runtime.addTrustedAgents(["agentPubKey"]); expect(r).toStrictEqual([ 'agentPubKey' ]) @@ -866,7 +890,7 @@ describe('Ad4mClient', () => { const verify = await ad4mClient.runtime.verifyStringSignedByDid("did", "didSigningKeyId", "data", "signedData") expect(verify).toBe(true) }) - + it('setStatus smoke test', async () => { const link = new LinkExpression() link.author = 'did:method:12345' @@ -984,13 +1008,13 @@ describe('Ad4mClient', () => { await ad4mClientWithoutSubscription.agent.updateDirectMessageLanguage("lang://test"); expect(agentUpdatedCallback).toBeCalledTimes(1) }) - + it('agent subscribeAgentStatusChanged smoke test', async () => { const agentStatusChangedCallback = jest.fn() ad4mClientWithoutSubscription.agent.addAgentStatusChangedListener(agentStatusChangedCallback) await new Promise(resolve => setTimeout(resolve, 100)) expect(agentStatusChangedCallback).toBeCalledTimes(0) - + ad4mClientWithoutSubscription.agent.subscribeAgentStatusChanged() await new Promise(resolve => setTimeout(resolve, 100)) await ad4mClientWithoutSubscription.agent.unlock("test", false); @@ -1002,13 +1026,13 @@ describe('Ad4mClient', () => { ad4mClientWithoutSubscription.agent.addAppChangedListener(appsChangedCallback) await new Promise(resolve => setTimeout(resolve, 100)) expect(appsChangedCallback).toBeCalledTimes(0) - + ad4mClientWithoutSubscription.agent.subscribeAppsChanged() await new Promise(resolve => setTimeout(resolve, 100)) await ad4mClientWithoutSubscription.agent.removeApp("test"); expect(appsChangedCallback).toBeCalledTimes(1) }) - + it('perspective subscribePerspectiveAdded smoke test', async () => { const perspectiveAddedCallback = jest.fn() ad4mClientWithoutSubscription.perspective.addPerspectiveAddedListener(perspectiveAddedCallback) @@ -1021,7 +1045,7 @@ describe('Ad4mClient', () => { await new Promise(resolve => setTimeout(resolve, 100)) expect(perspectiveAddedCallback).toBeCalledTimes(1) }) - + it('perspective subscribePerspectiveUpdated smoke test', async () => { const perspectiveUpdatedCallback = jest.fn() ad4mClientWithoutSubscription.perspective.addPerspectiveUpdatedListener(perspectiveUpdatedCallback) @@ -1034,7 +1058,7 @@ describe('Ad4mClient', () => { await new Promise(resolve => setTimeout(resolve, 100)) expect(perspectiveUpdatedCallback).toBeCalledTimes(1) }) - + it('perspective subscribePerspectiveRemoved smoke test', async () => { const perspectiveRemovedCallback = jest.fn() ad4mClientWithoutSubscription.perspective.addPerspectiveRemovedListener(perspectiveRemovedCallback) @@ -1051,7 +1075,7 @@ describe('Ad4mClient', () => { describe('ad4mClient with subscription', () => { let ad4mClientWithSubscription - + beforeEach(() => { ad4mClientWithSubscription = new Ad4mClient(apolloClient, true) }) @@ -1061,18 +1085,18 @@ describe('Ad4mClient', () => { ad4mClientWithSubscription.agent.addUpdatedListener(agentUpdatedCallback) await new Promise(resolve => setTimeout(resolve, 100)) expect(agentUpdatedCallback).toBeCalledTimes(0) - + await new Promise(resolve => setTimeout(resolve, 100)) await ad4mClientWithSubscription.agent.updateDirectMessageLanguage("lang://test"); expect(agentUpdatedCallback).toBeCalledTimes(1) }) - + it('agent subscribeAgentStatusChanged smoke test', async () => { const agentStatusChangedCallback = jest.fn() ad4mClientWithSubscription.agent.addAgentStatusChangedListener(agentStatusChangedCallback) await new Promise(resolve => setTimeout(resolve, 100)) expect(agentStatusChangedCallback).toBeCalledTimes(0) - + await new Promise(resolve => setTimeout(resolve, 100)) await ad4mClientWithSubscription.agent.unlock("test", false); expect(agentStatusChangedCallback).toBeCalledTimes(1) @@ -1083,12 +1107,12 @@ describe('Ad4mClient', () => { ad4mClientWithSubscription.agent.addAppChangedListener(appsChangedCallback) await new Promise(resolve => setTimeout(resolve, 100)) expect(appsChangedCallback).toBeCalledTimes(0) - + await new Promise(resolve => setTimeout(resolve, 100)) await ad4mClientWithSubscription.agent.removeApp("test"); expect(appsChangedCallback).toBeCalledTimes(1) }) - + it('perspective subscribePerspectiveAdded smoke test', async () => { const perspectiveAddedCallback = jest.fn() ad4mClientWithSubscription.perspective.addPerspectiveAddedListener(perspectiveAddedCallback) @@ -1100,7 +1124,7 @@ describe('Ad4mClient', () => { await new Promise(resolve => setTimeout(resolve, 100)) expect(perspectiveAddedCallback).toBeCalledTimes(1) }) - + it('perspective subscribePerspectiveUpdated smoke test', async () => { const perspectiveUpdatedCallback = jest.fn() ad4mClientWithSubscription.perspective.addPerspectiveUpdatedListener(perspectiveUpdatedCallback) @@ -1112,13 +1136,13 @@ describe('Ad4mClient', () => { await new Promise(resolve => setTimeout(resolve, 100)) expect(perspectiveUpdatedCallback).toBeCalledTimes(1) }) - + it('perspective subscribePerspectiveRemoved smoke test', async () => { const perspectiveRemovedCallback = jest.fn() ad4mClientWithSubscription.perspective.addPerspectiveRemovedListener(perspectiveRemovedCallback) await new Promise(resolve => setTimeout(resolve, 100)) expect(perspectiveRemovedCallback).toBeCalledTimes(0) - + await new Promise(resolve => setTimeout(resolve, 100)) await ad4mClientWithSubscription.perspective.remove('00006'); await new Promise(resolve => setTimeout(resolve, 100)) diff --git a/core/src/perspectives/PerspectiveClient.ts b/core/src/perspectives/PerspectiveClient.ts index f67be7dce..209a4729c 100644 --- a/core/src/perspectives/PerspectiveClient.ts +++ b/core/src/perspectives/PerspectiveClient.ts @@ -292,6 +292,33 @@ export class PerspectiveClient { })).perspectiveAddSdna } + async executeCommands(uuid: string, commands: string, expression: string, parameters: string): Promise { + return unwrapApolloResult(await this.#apolloClient.mutate({ + mutation: gql`mutation perspectiveExecuteCommands($uuid: String!, $commands: String!, $expression: String!, $parameters: String) { + perspectiveExecuteCommands(uuid: $uuid, commands: $commands, expression: $expression, parameters: $parameters) + }`, + variables: { uuid, commands, expression, parameters } + })).perspectiveExecuteCommands + } + + async createSubject(uuid: string, subjectClass: string, expressionAddress: string): Promise { + return unwrapApolloResult(await this.#apolloClient.mutate({ + mutation: gql`mutation perspectiveCreateSubject($uuid: String!, $subjectClass: String!, $expressionAddress: String!) { + perspectiveCreateSubject(uuid: $uuid, subjectClass: $subjectClass, expressionAddress: $expressionAddress) + }`, + variables: { uuid, subjectClass, expressionAddress } + })).perspectiveCreateSubject + } + + async getSubjectData(uuid: string, subjectClass: string, expressionAddress: string): Promise { + return unwrapApolloResult(await this.#apolloClient.mutate({ + mutation: gql`mutation perspectiveGetSubjectData($uuid: String!, $subjectClass: String!, $expressionAddress: String!) { + perspectiveGetSubjectData(uuid: $uuid, subjectClass: $subjectClass, expressionAddress: $expressionAddress) + }`, + variables: { uuid, subjectClass, expressionAddress } + })).perspectiveGetSubjectData + } + // ExpressionClient functions, needed for Subjects: async getExpression(expressionURI: string): Promise { return await this.#expressionClient.get(expressionURI) diff --git a/core/src/perspectives/PerspectiveProxy.ts b/core/src/perspectives/PerspectiveProxy.ts index c1f580382..62bcfbc5f 100644 --- a/core/src/perspectives/PerspectiveProxy.ts +++ b/core/src/perspectives/PerspectiveProxy.ts @@ -66,55 +66,7 @@ export class PerspectiveProxy { } async executeAction(actions, expression, parameters: Parameter[]) { - const replaceThis = (input: string|undefined) => { - if(input) { - if (input === 'this') { - return expression - } else { - return input - } - } else { - return undefined - } - } - - const replaceParameters = (input: string|undefined) => { - if(parameters) { - let output = input - for(const parameter of parameters) { - output = output.replace(parameter.name, parameter.value) - } - return output - } else - return input - } - - for(let command of actions) { - let source = replaceThis(replaceParameters(command.source)) - let predicate = replaceThis(replaceParameters(command.predicate)) - let target = replaceThis(replaceParameters(command.target)) - let local = command?.local ?? false - - switch(command.action) { - case 'addLink': - await this.add(new Link({source, predicate, target}), local ? 'local' : 'shared') - break; - case 'removeLink': - const linkExpressions = await this.get(new LinkQuery({source, predicate, target})) - for (const linkExpression of linkExpressions) { - await this.remove(linkExpression) - } - break; - case 'setSingleTarget': - await this.setSingleTarget(new Link({source, predicate, target}), local ? 'local' : 'shared') - break; - case 'collectionSetter': - const links = await this.get(new LinkQuery({ source, predicate })) - await this.removeLinks(links); - await this.addLinks(parameters.map(p => new Link({source, predicate, target: p.value})), local ? 'local' : 'shared') - break; - } - } + return await this.#client.executeCommands(this.#handle.uuid, JSON.stringify(actions), expression, JSON.stringify(parameters)) } /** Returns all the links of this perspective that matches the LinkQuery */ @@ -364,17 +316,28 @@ export class PerspectiveProxy { * @param exprAddr The address of the expression to be turned into a subject instance */ async createSubject(subjectClass: T, exprAddr: string): Promise { - let className = await this.stringOrTemplateObjectToSubjectClass(subjectClass) - let result = await this.infer(`subject_class("${className}", C), constructor(C, Actions)`) - if(!result.length) { - throw "No constructor found for given subject class: " + className + let className: string; + + if(typeof subjectClass === "string") { + className = subjectClass + + await this.#client.createSubject(this.#handle.uuid, JSON.stringify({className}), exprAddr); + } else { + let query = this.buildQueryFromTemplate(subjectClass as object) + await this.#client.createSubject(this.#handle.uuid, JSON.stringify({query}), exprAddr); } - let actions = result.map(x => eval(x.Actions)) - await this.executeAction(actions[0], exprAddr, undefined) return this.getSubjectProxy(exprAddr, subjectClass) } + async getSubjectData(subjectClass: T, exprAddr: string): Promise { + if (typeof subjectClass === "string") { + return JSON.parse(await this.#client.getSubjectData(this.#handle.uuid, JSON.stringify({className: subjectClass}), exprAddr)) + } + let query = this.buildQueryFromTemplate(subjectClass as object) + return JSON.parse(await this.#client.getSubjectData(this.#handle.uuid, JSON.stringify({query}), exprAddr)) + } + /** Removes a subject instance by running its (SDNA defined) destructor, * which means removing links around the given expression address * @@ -468,18 +431,7 @@ export class PerspectiveProxy { } - /** Returns all subject classes that match the given template object. - * This function looks at the properties of the template object and - * its setters and collections to create a Prolog query that finds - * all subject classes that would be converted to a proxy object - * with exactly the same properties and collections. - * - * Since there could be multiple subject classes that match the given - * criteria, this function returns a list of class names. - * - * @param obj The template object - */ - async subjectClassesByTemplate(obj: object): Promise { + private buildQueryFromTemplate(obj: object): string { // Collect all string properties of the object in a list let properties = [] @@ -550,6 +502,23 @@ export class PerspectiveProxy { query += "." + + return query; + } + + /** Returns all subject classes that match the given template object. + * This function looks at the properties of the template object and + * its setters and collections to create a Prolog query that finds + * all subject classes that would be converted to a proxy object + * with exactly the same properties and collections. + * + * Since there could be multiple subject classes that match the given + * criteria, this function returns a list of class names. + * + * @param obj The template object + */ + async subjectClassesByTemplate(obj: object): Promise { + const query = this.buildQueryFromTemplate(obj); let result = await this.infer(query) if(!result) { return [] diff --git a/core/src/perspectives/PerspectiveResolver.ts b/core/src/perspectives/PerspectiveResolver.ts index f70128d45..b3dd886cc 100644 --- a/core/src/perspectives/PerspectiveResolver.ts +++ b/core/src/perspectives/PerspectiveResolver.ts @@ -181,12 +181,40 @@ export default class PerspectiveResolver { pubSub.publish(LINK_REMOVED_TOPIC) return true } - + @Mutation(returns => Boolean) perspectiveAddSdna(@Arg('uuid') uuid: string, @Arg('name') name: string, @Arg('sdnaCode') sdnaCode: string, @Arg('sdnaType') sdnaType: string, @PubSub() pubSub: any): Boolean { return true } + @Mutation(returns => Boolean) + perspectiveExecuteCommands( + @Arg('uuid') uuid: string, + @Arg('commands') commands: string, + @Arg('expression') expression: string, + @Arg('parameters', type => String, {nullable: true}) parameters: string + ): Boolean { + return true + } + + @Mutation(returns => Boolean) + perspectiveCreateSubject( + @Arg('uuid') uuid: string, + @Arg('subjectClass') SubjectClass: string, + @Arg('expressionAddress') expressionAddress: string + ): Boolean { + return true + } + + @Mutation(returns => String) + perspectiveGetSubjectData( + @Arg('uuid') uuid: string, + @Arg('subjectClass') SubjectClass: string, + @Arg('expressionAddress') expressionAddress: string + ): String { + return "" + } + @Subscription({topics: PERSPECTIVE_ADDED_TOPIC, nullable: true}) perspectiveAdded(): PerspectiveHandle { const perspective = new PerspectiveHandle('00001', 'New Perspective'); diff --git a/core/src/subject/SubjectEntity.ts b/core/src/subject/SubjectEntity.ts index b65d15eae..581439958 100644 --- a/core/src/subject/SubjectEntity.ts +++ b/core/src/subject/SubjectEntity.ts @@ -43,63 +43,11 @@ export class SubjectEntity { private async getData(id?: string) { const tempId = id ?? this.#baseExpression; - let isInstance = await this.#perspective.isSubjectInstance(tempId, this.#subjectClass) - if (!isInstance) { - throw `Not a valid subject instance of ${this.#subjectClass} for ${tempId}` - } - - let results = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property(C, Property)`) - let properties = results.map(result => result.Property) - - for (let p of properties) { - const resolveExpressionURI = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property_resolve(C, "${p}")`) - const getProperty = async () => { - let results = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), property_getter(C, "${tempId}", "${p}", Value)`) - if (results && results.length > 0) { - let expressionURI = results[0].Value - if (resolveExpressionURI) { - try { - const expression = await this.#perspective.getExpression(expressionURI) - try { - return JSON.parse(expression.data) - } catch (e) { - return expression.data - } - } catch (err) { - return expressionURI - } - } else { - return expressionURI - } - } else if (results) { - return results - } else { - return undefined - } - }; - - this[p] = await getProperty() - } - - let results2 = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection(C, Collection)`) - if (!results2) results2 = [] - let collections = results2.map(result => result.Collection) - - for (let c of collections) { - const getProperty = async () => { - let results = await this.#perspective.infer(`subject_class("${this.#subjectClass}", C), collection_getter(C, "${tempId}", "${c}", Value)`) - if (results && results.length > 0 && results[0].Value) { - return eval(results[0].Value) - } else { - return [] - } - } - - this[c] = await getProperty() - } - + console.log("SubjectEntity: getData") + let data = await this.#perspective.getSubjectData(this.#subjectClass, tempId) + console.log("SubjectEntity got data:", data) + Object.assign(this, data); this.#baseExpression = tempId; - return this } diff --git a/rust-executor/Cargo.toml b/rust-executor/Cargo.toml index 9dc31d0cb..35151de2f 100644 --- a/rust-executor/Cargo.toml +++ b/rust-executor/Cargo.toml @@ -89,6 +89,7 @@ rusqlite = { version = "0.29.0", features = ["bundled"] } fake = { version = "2.9.2", features = ["derive"] } sha2 = "0.10.8" regex = "1.5.4" +json5 = "0.4" include_dir = "0.6.0" diff --git a/rust-executor/src/graphql/mutation_resolvers.rs b/rust-executor/src/graphql/mutation_resolvers.rs index 5280b7620..c8e9699bf 100644 --- a/rust-executor/src/graphql/mutation_resolvers.rs +++ b/rust-executor/src/graphql/mutation_resolvers.rs @@ -1,7 +1,7 @@ #![allow(non_snake_case)] - -use crate::{db::Ad4mDb, runtime_service::{RuntimeService}, types::Notification}; - +use std::collections::HashMap; +use crate::{db::Ad4mDb, types::Notification, perspectives::perspective_instance::{Command, Parameter, SubjectClass, SubjectClassOption}, runtime_service::{self, RuntimeService}}; +use ad4m_client::literal::Literal; use crate::{agent::create_signed_expression, neighbourhoods::{self, install_neighbourhood}, perspectives::{add_perspective, get_perspective, perspective_instance::{PerspectiveInstance, SdnaType}, remove_perspective, update_perspective}, types::{DecoratedLinkExpression, Link, LinkExpression}}; use coasys_juniper::{graphql_object, graphql_value, FieldResult, FieldError}; @@ -833,6 +833,88 @@ impl Mutation { Ok(true) } + async fn perspective_execute_commands( + &self, + context: &RequestContext, + uuid: String, + commands: String, + expression: String, + parameters: Option, + ) -> FieldResult { + check_capability( + &context.capabilities, + &perspective_update_capability(vec![uuid.clone()]), + )?; + + let commands: Vec = serde_json::from_str(&commands) + .map_err(|e| FieldError::new( + e, + graphql_value!({ "invalid_commands": commands }) + ))?; + let parameters: Vec = if let Some(p) = parameters { + serde_json::from_str(&p) + .map_err(|e| FieldError::new( + e, + graphql_value!({ "invalid_parameters": p }) + ))? + } else { + Vec::new() + }; + + let mut perspective = get_perspective_with_uuid_field_error(&uuid)?; + perspective.execute_commands(commands, expression, parameters).await?; + Ok(true) + } + + async fn perspective_create_subject( + &self, + context: &RequestContext, + uuid: String, + subject_class: String, + expression_address: String, + ) -> FieldResult { + check_capability( + &context.capabilities, + &perspective_update_capability(vec![uuid.clone()]), + )?; + + let subject_class: SubjectClassOption = serde_json::from_str(&subject_class) + .map_err(|e| FieldError::new( + e, + graphql_value!({ "invalid_subject_class": subject_class }) + ))?; + + let mut perspective = get_perspective_with_uuid_field_error(&uuid)?; + + perspective.create_subject(subject_class, expression_address).await?; + Ok(true) + } + + + async fn perspective_get_subject_data( + &self, + context: &RequestContext, + uuid: String, + subject_class: String, + expression_address: String, + ) -> FieldResult { + check_capability( + &context.capabilities, + &perspective_update_capability(vec![uuid.clone()]), + )?; + + let subject_class: SubjectClassOption = serde_json::from_str(&subject_class) + .map_err(|e| FieldError::new( + e, + graphql_value!({ "invalid_subject_class": subject_class }) + ))?; + + let mut perspective = get_perspective_with_uuid_field_error(&uuid)?; + + let result = perspective.get_subject_data(subject_class, expression_address).await?; + Ok(result) + } + async fn runtime_add_friends( &self, context: &RequestContext, diff --git a/rust-executor/src/js_core/mod.rs b/rust-executor/src/js_core/mod.rs index 6bb449168..4ff43475c 100644 --- a/rust-executor/src/js_core/mod.rs +++ b/rust-executor/src/js_core/mod.rs @@ -34,7 +34,7 @@ use self::futures::{EventLoopFuture, SmartGlobalVariableFuture}; use crate::holochain_service::maybe_get_holochain_service; use crate::Ad4mConfig; -static JS_CORE_HANDLE: Lazy>>> = +pub(crate) static JS_CORE_HANDLE: Lazy>>> = Lazy::new(|| Arc::new(TokioMutex::new(None))); pub struct JsCoreHandle { diff --git a/rust-executor/src/perspectives/perspective_instance.rs b/rust-executor/src/perspectives/perspective_instance.rs index 09b315c26..4795230a8 100644 --- a/rust-executor/src/perspectives/perspective_instance.rs +++ b/rust-executor/src/perspectives/perspective_instance.rs @@ -1,6 +1,7 @@ -use std::collections::BTreeMap; +use std::collections::{self, HashMap, BTreeMap}; use std::sync::Arc; use std::time::Duration; +use serde_json::Value; use scryer_prolog::machine::parsed_results::{QueryMatch, QueryResolution}; use tokio::{join, time}; use tokio::sync::Mutex; @@ -12,13 +13,15 @@ use serde::{Serialize, Deserialize}; use crate::agent::create_signed_expression; use crate::languages::language::Language; use crate::languages::LanguageController; +use crate::perspectives::utils::{prolog_get_first_binding, prolog_value_to_json_string}; use crate::prolog_service::engine::PrologEngine; use crate::pubsub::{get_global_pubsub, NEIGHBOURHOOD_SIGNAL_TOPIC, PERSPECTIVE_LINK_ADDED_TOPIC, PERSPECTIVE_LINK_REMOVED_TOPIC, PERSPECTIVE_LINK_UPDATED_TOPIC, PERSPECTIVE_SYNC_STATE_CHANGE_TOPIC, RUNTIME_NOTIFICATION_TRIGGERED_TOPIC}; use crate::{db::Ad4mDb, types::*}; -use crate::graphql::graphql_types::{DecoratedPerspectiveDiff, LinkMutations, LinkQuery, LinkStatus, NeighbourhoodSignalFilter, OnlineAgent, PerspectiveExpression, PerspectiveHandle, PerspectiveLinkFilter, PerspectiveLinkUpdatedFilter, PerspectiveState, PerspectiveStateFilter}; +use crate::graphql::graphql_types::{DecoratedPerspectiveDiff, ExpressionRendered, JsResultType, LinkMutations, LinkQuery, LinkStatus, NeighbourhoodSignalFilter, OnlineAgent, PerspectiveExpression, PerspectiveHandle, PerspectiveLinkFilter, PerspectiveLinkUpdatedFilter, PerspectiveState, PerspectiveStateFilter}; use super::sdna::init_engine_facts; use super::update_perspective; -use super::utils::prolog_resolution_to_string; +use super::utils::{prolog_get_all_string_bindings, prolog_get_first_string_binding, prolog_resolution_to_string}; +use json5; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] @@ -39,6 +42,82 @@ impl SdnaType { } } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub enum Action { + #[serde(rename = "addLink")] + AddLink, + #[serde(rename = "removeLink")] + RemoveLink, + #[serde(rename = "setSingleTarget")] + SetSingleTarget, + #[serde(rename = "collectionSetter")] + CollectionSetter, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct Command { + source: Option, + predicate: Option, + target: Option, + local: Option, + action: Action, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct SubjectClass { + #[serde(rename = "C")] + c: Option, + #[serde(rename = "Class")] + class: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct SubjectClassProperty { + #[serde(rename = "C")] + c: Option, + #[serde(rename = "Property")] + property: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct SubjectClassCollection { + #[serde(rename = "C")] + c: Option, + #[serde(rename = "Collection")] + collection: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct SubjectClassActions { + #[serde(rename = "C")] + c: Option, + #[serde(rename = "Actions")] + actions: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct PorpertyValue { + #[serde(rename = "C")] + c: Option, + #[serde(rename = "Value")] + value: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct SubjectClassOption { + #[serde(rename = "className")] + class_name: Option, + #[serde(rename = "query")] + query: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct Parameter { + name: String, + value: serde_json::Value, +} + + #[derive(Clone)] pub struct PerspectiveInstance { pub persisted: Arc>, @@ -148,7 +227,7 @@ impl PerspectiveInstance { if link_language.current_revision().await.map_err(|e| anyhow!("current_revision error: {}",e))?.is_some() { // Ok, we are synced and have a revision. Let's commit our pending diffs. let pending_diffs = Ad4mDb::with_global_instance(|db| db.get_pending_diffs(&uuid)).map_err(|e| anyhow!("get_pending_diffs error: {}",e))?; - + if pending_diffs.additions.is_empty() && pending_diffs.removals.is_empty() { return Ok(()); } @@ -290,7 +369,7 @@ impl PerspectiveInstance { tokio::spawn(async move { if let Err(_) = self_clone.commit(&diff_clone).await { let handle_clone = self_clone.persisted.lock().await.clone(); - Ad4mDb::with_global_instance(|db| + Ad4mDb::with_global_instance(|db| db.add_pending_diff(&handle_clone.uuid, &diff_clone) ).expect("Couldn't write pending diff. DB should be initialized and usable at this point"); } @@ -301,13 +380,13 @@ impl PerspectiveInstance { let handle = self.persisted.lock().await.clone(); let notification_snapshot_before = self.notification_trigger_snapshot().await; if !diff.additions.is_empty() { - Ad4mDb::with_global_instance(|db| + Ad4mDb::with_global_instance(|db| db.add_many_links(&handle.uuid, diff.additions.clone(), &LinkStatus::Shared) ).expect("Failed to add many links"); } if !diff.removals.is_empty() { - Ad4mDb::with_global_instance(|db| + Ad4mDb::with_global_instance(|db| for link in &diff.removals { db.remove_link(&handle.uuid, link).expect("Failed to remove link"); } @@ -315,7 +394,7 @@ impl PerspectiveInstance { } let decorated_diff = DecoratedPerspectiveDiff { - additions: diff.additions.iter().map(|link| DecoratedLinkExpression::from((link.clone(), LinkStatus::Shared))).collect(), + additions: diff.additions.iter().map(|link| DecoratedLinkExpression::from((link.clone(), LinkStatus::Shared))).collect(), removals: diff.removals.iter().map(|link| DecoratedLinkExpression::from((link.clone(), LinkStatus::Shared))).collect() }; @@ -509,7 +588,7 @@ impl PerspectiveInstance { ) .await; - + if link_status == LinkStatus::Shared { self.spawn_commit_and_handle_error(&diff); } @@ -729,7 +808,7 @@ impl PerspectiveInstance { /// Executes a Prolog query against the engine, spawning and initializing the engine if necessary. pub async fn prolog_query(&self, query: String) -> Result { self.ensure_prolog_engine().await?; - + let prolog_engine_mutex = self.prolog_engine.lock().await; let prolog_engine_option_ref = prolog_engine_mutex.as_ref(); let prolog_engine = prolog_engine_option_ref.as_ref().expect("Must be some since we initialized the engine above"); @@ -751,7 +830,7 @@ impl PerspectiveInstance { tokio::spawn(async move { let uuid = self_clone.persisted.lock().await.uuid.clone(); - + if let Err(e) = self_clone.ensure_prolog_engine().await { log::error!("Error spawning Prolog engine: {:?}", e) }; @@ -763,7 +842,7 @@ impl PerspectiveInstance { } else { self_clone.pubsub_publish_diff(diff).await; let after = self_clone.notification_trigger_snapshot().await; - let new_matches = Self::subtract_before_notification_matches(before, after); + let new_matches = Self::subtract_before_notification_matches(before, after); Self::publish_notification_matches(uuid, new_matches).await; } }); @@ -841,7 +920,7 @@ impl PerspectiveInstance { Ok(()) } - + async fn no_link_language_error(&self) -> AnyError { let handle = self.persisted.lock().await.clone(); anyhow!("Perspective {} has no link language installed. State is: {:?}", handle.uuid, handle.state) @@ -922,8 +1001,227 @@ impl PerspectiveInstance { } + pub async fn execute_commands(&mut self, commands: Vec, expression: String, parameters: Vec) -> Result<(), AnyError> { + let jsvalue_to_string = |value: &Value| -> String { + match value { + serde_json::Value::String(s) => s.clone(), + _ => value.to_string(), + } + }; + + let replace_this = |input: Option| -> Option { + if Some(String::from("this")) == input { + Some(expression.clone()) + } else { + input + } + }; + + let replace_parameters = |input: Option| -> Option { + if let Some(mut output) = input { + for parameter in ¶meters { + output = output.replace(¶meter.name, &jsvalue_to_string(¶meter.value)); + } + Some(output) + } else { + input + } + }; + + for command in commands { + let source = replace_this(replace_parameters(command.source)) + .ok_or_else(|| anyhow!("Source cannot be None"))?; + let predicate = replace_this(replace_parameters(command.predicate)); + let target = (replace_parameters(command.target)) + .ok_or_else(|| anyhow!("Source cannot be None"))?; + let local = command.local.unwrap_or(false); + let status = if local { LinkStatus::Local } else { LinkStatus::Shared }; + + match command.action { + Action::AddLink => { + self.add_link(Link{ source, predicate, target }, status).await?; + } + Action::RemoveLink => { + let link_expressions = self.get_links(&LinkQuery{ + source:Some(source), + predicate, + target: Some(target), + from_date: None, + until_date: None, + limit: None + }).await?; + for link_expression in link_expressions { + self.remove_link(link_expression.into()).await?; + } + } + Action::SetSingleTarget => { + let link_expressions = self.get_links(&LinkQuery{ + source:Some(source.clone()), + predicate: predicate.clone(), + target: None, + from_date: None, + until_date: None, + limit: None + }).await?; + for link_expression in link_expressions { + self.remove_link(link_expression.into()).await?; + } + self.add_link(Link{ source, predicate, target }, status).await?; + } + Action::CollectionSetter => { + let link_expressions = self.get_links(&LinkQuery{ + source:Some(source.clone()), + predicate: predicate.clone(), + target: None, + from_date: None, + until_date: None, + limit: None + }).await?; + for link_expression in link_expressions { + self.remove_link(link_expression.into()).await?; + } + self.add_links( + parameters.iter().map(|p| Link{ + source: source.clone(), + predicate: predicate.clone(), + target: jsvalue_to_string(&p.value) + }).collect(), + status + ).await?; + } + } + } + + Ok(()) + } + + async fn subject_class_option_to_class_name(&mut self, subject_class: SubjectClassOption) -> Result { + Ok(if subject_class.class_name.is_some() { + subject_class.class_name.unwrap() + } else { + let query = subject_class.query.ok_or(anyhow!("SubjectClassOption needs to either have `name` or `query` set"))?; + let result = self.prolog_query(format!("{}", query)).await + .map_err(|e| { + log::error!("Error creating subject: {:?}", e); + e + })?; + prolog_get_first_string_binding(&result, "Class") + .map(|value| value.clone()) + .ok_or(anyhow!("No matching subject class found!"))? + }) + } + + + pub async fn create_subject(&mut self, subject_class: SubjectClassOption, expression_address: String) -> Result<(), AnyError> { + let class_name = self.subject_class_option_to_class_name(subject_class).await?; + let result = self.prolog_query(format!("subject_class(\"{}\", C), constructor(C, Actions).", class_name)).await?; + let actions = prolog_get_first_string_binding(&result, "Actions") + .ok_or(anyhow!("No constructor found for class: {}", class_name))?; + + + let commands: Vec = json5::from_str(&actions).unwrap(); + self.execute_commands(commands, expression_address, vec![]).await?; + Ok(()) + } + + pub async fn get_subject_data(&mut self, subject_class: SubjectClassOption, base_expression: String) -> Result{ + let mut object: HashMap = HashMap::new(); + + let class_name = self.subject_class_option_to_class_name(subject_class).await?; + let result = self.prolog_query(format!("subject_class(\"{}\", C), instance(C, \"{}\").", class_name, base_expression)).await?; + + if let QueryResolution::False = result { + log::error!("No instance found for class: {} with id: {}", class_name, base_expression); + return Err(anyhow!("No instance found for class: {} with id: {}", class_name, base_expression)); + } + + let properties_result = self.prolog_query(format!(r#"subject_class("{}", C), property(C, Property)."#, class_name)).await?; + let properties: Vec = prolog_get_all_string_bindings(&properties_result, "Property"); + + for p in &properties { + let property_values_result = self.prolog_query(format!(r#"subject_class("{}", C), property_getter(C, "{}", "{}", Value)"#, class_name, base_expression, p)).await?; + if let Some(property_value) = prolog_get_first_binding(&property_values_result, "Value") { + let result = self.prolog_query(format!(r#"subject_class("{}", C), property_resolve(C, "{}")"#, class_name, p)).await?; + println!("resolve query result for {}: {:?}", p, result); + let resolve_expression_uri = QueryResolution::False != result; + println!("resolve_expression_uri for {}: {:?}", p, resolve_expression_uri); + let value = if resolve_expression_uri { + match &property_value { + scryer_prolog::machine::parsed_results::Value::String(s) => { + println!("getting expr url: {}", s); + let mut lock = crate::js_core::JS_CORE_HANDLE.lock().await; + + if let Some(ref mut js) = *lock { + let result = js.execute(format!( + r#"JSON.stringify(await core.callResolver("Query", "expression", {{ url: "{}" }}))"#, + s + )) + .await?; + + let result: JsResultType> = serde_json::from_str(&result)?; + + match result { + JsResultType::Ok(Some(expr)) => expr.data, + JsResultType::Ok(None) | JsResultType::Error(_) => prolog_value_to_json_string(property_value.clone()), + } + } else { + prolog_value_to_json_string(property_value.clone()) + } + }, + x => { + println!("Couldn't get expression subjectentity: {:?}", x); + prolog_value_to_json_string(property_value.clone()) + } + } + } else { + prolog_value_to_json_string(property_value.clone()) + }; + object.insert(p.clone(), value); + } else { + log::error!("Couldn't get a property value for class: `{}`, property: `{}`, base: `{}`\nProlog query result was: {:?}", class_name, p, base_expression, property_values_result); + object.insert(p.clone(), "null".to_string()); + }; + } + + let collections_results = self.prolog_query(format!(r#"subject_class("{}", C), collection(C, Collection)"#, class_name)).await?; + let collections: Vec = prolog_get_all_string_bindings(&collections_results, "Collection"); + + for c in collections { + let collection_values_result = self.prolog_query(format!(r#"subject_class("{}", C), collection_getter(C, "{}", "{}", Value)"#, class_name, base_expression, c)).await?; + if let Some(collection_value) = prolog_get_first_binding(&collection_values_result, "Value") { + object.insert(c.clone(), prolog_value_to_json_string(collection_value)); + } else { + log::error!("Couldn't get a collection value for class: `{}`, collection: `{}`, base: `{}`\nProlog query result was: {:?}", class_name, c, base_expression, collection_values_result); + object.insert(c.clone(), "[]".to_string()); + } + } + + let stringified = object.into_iter() + .map(|(k, v)| { + format!(r#""{}": {}"#, k, v) + }) + .collect::>() + .join(", "); + + Ok(format!("{{ {} }}", stringified)) + } } +pub fn prolog_result(result: String) -> Value { + let v: Value = serde_json::from_str(&result).unwrap(); + match v { + Value::String(string) => { + if string == "true" { + Value::Bool(true) + } else if string == "false" { + Value::Bool(false) + } else { + Value::String(string) + } + } + _ => v, + } +} diff --git a/rust-executor/src/perspectives/utils.rs b/rust-executor/src/perspectives/utils.rs index 6a938382b..66b8a19c3 100644 --- a/rust-executor/src/perspectives/utils.rs +++ b/rust-executor/src/perspectives/utils.rs @@ -1,6 +1,6 @@ use scryer_prolog::machine::parsed_results::{Value, QueryMatch, QueryResolution}; -pub fn prolog_value_to_json_tring(value: Value) -> String { +pub fn prolog_value_to_json_string(value: Value) -> String { match value { Value::Integer(i) => format!("{}", i), Value::Float(f) => format!("{}", f), @@ -25,7 +25,7 @@ pub fn prolog_value_to_json_tring(value: Value) -> String { if i > 0 { string_result.push_str(", "); } - string_result.push_str(&prolog_value_to_json_tring(v.clone())); + string_result.push_str(&prolog_value_to_json_string(v.clone())); } string_result.push_str("]"); string_result @@ -36,7 +36,7 @@ pub fn prolog_value_to_json_tring(value: Value) -> String { if i > 0 { string_result.push_str(", "); } - string_result.push_str(&prolog_value_to_json_tring(v.clone())); + string_result.push_str(&prolog_value_to_json_string(v.clone())); } string_result.push_str("]"); string_result @@ -51,7 +51,7 @@ fn prolog_match_to_json_string(query_match: &QueryMatch) -> String { if i > 0 { string_result.push_str(", "); } - string_result.push_str(&format!("\"{}\": {}", k, prolog_value_to_json_tring(v.clone()))); + string_result.push_str(&format!("\"{}\": {}", k, prolog_value_to_json_string(v.clone()))); } string_result.push_str("}"); string_result @@ -69,4 +69,38 @@ pub fn prolog_resolution_to_string(resultion: QueryResolution) -> String { format!("[{}]", matches_json.join(", ")) } } +} + +pub fn prolog_get_first_string_binding(result: &QueryResolution, variable_name: &str) -> Option { + prolog_get_all_string_bindings(result, variable_name).into_iter().next() +} + +pub fn prolog_get_all_string_bindings(result: &QueryResolution, variable_name: &str) -> Vec { + if let QueryResolution::Matches(matches) = result { + matches.iter() + .filter_map(|m| m.bindings.get(variable_name)) + .filter_map(|value| match value { + scryer_prolog::machine::parsed_results::Value::String(s) => Some(s), + _ => None, + }) + .cloned() + .collect() + } else { + Vec::new() + } +} + +pub fn prolog_get_first_binding(result: &QueryResolution, variable_name: &str) -> Option { + prolog_get_all_bindings(result, variable_name).into_iter().next() +} + +pub fn prolog_get_all_bindings(result: &QueryResolution, variable_name: &str) -> Vec { + if let QueryResolution::Matches(matches) = result { + matches.iter() + .filter_map(|m| m.bindings.get(variable_name)) + .cloned() + .collect() + } else { + Vec::new() + } } \ No newline at end of file diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index 31da7256a..dc36fead1 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -593,6 +593,14 @@ describe("Prolog + Literals", () => { local: true }) local: string = "" + + @SubjectProperty({ + through: "recipe://resolve", + writable: true, + resolveLanguage: "literal" + }) + resolve: string = "" + } before(async () => { @@ -652,6 +660,7 @@ describe("Prolog + Literals", () => { const recipe2 = new Recipe(perspective!, root); await recipe2.get(); + console.log("comments:", recipe2.comments) expect(recipe2.comments.length).to.equal(2) }) @@ -727,6 +736,22 @@ describe("Prolog + Literals", () => { expect(recipe2.ingredients.length).to.equal(1) }) + + it("can implement the resolveLanguage property type", async () => { + let root = Literal.from("Active record implementation test resolveLanguage").toUrl() + const recipe = new Recipe(perspective!, root) + + recipe.resolve = "Test name literal"; + + await recipe.save(); + await recipe.get(); + + //@ts-ignore + let links = await perspective!.get(new LinkQuery({source: root, predicate: "recipe://resolve"})) + expect(links.length).to.equal(1) + let literal = Literal.fromUrl(links[0].data.target).get() + expect(literal.data).to.equal(recipe.resolve) + }) }) }) })