From cd9c991de3bcb9a50d48cfdee72d09ce79385280 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Thu, 20 Mar 2025 23:36:44 +0100 Subject: [PATCH 01/15] rename decorators file --- core/src/index.ts | 2 +- core/src/subject/SubjectEntity.ts | 2 +- core/src/subject/{SDNADecorators.ts => decorators.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename core/src/subject/{SDNADecorators.ts => decorators.ts} (100%) diff --git a/core/src/index.ts b/core/src/index.ts index 019a8d4db..320905dca 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -19,7 +19,7 @@ export * from "./perspectives/PerspectiveProxy"; export * from "./perspectives/PerspectiveDiff"; export * from "./perspectives/LinkQuery"; export * from "./SmartLiteral"; -export * from "./subject/SDNADecorators"; +export * from "./subject/decorators"; export * from "./subject/Subject"; export * from "./neighbourhood/Neighbourhood"; export * from "./neighbourhood/NeighbourhoodProxy"; diff --git a/core/src/subject/SubjectEntity.ts b/core/src/subject/SubjectEntity.ts index 821e4f58d..a9e268134 100644 --- a/core/src/subject/SubjectEntity.ts +++ b/core/src/subject/SubjectEntity.ts @@ -1,7 +1,7 @@ import { Literal } from "../Literal"; import { Link } from "../links/Links"; import { PerspectiveProxy } from "../perspectives/PerspectiveProxy"; -import { makeRandomPrologAtom } from "./SDNADecorators"; +import { makeRandomPrologAtom } from "./decorators"; import { singularToPlural } from "./util"; type ValueTuple = [name: string, value: any, resolve?: boolean]; diff --git a/core/src/subject/SDNADecorators.ts b/core/src/subject/decorators.ts similarity index 100% rename from core/src/subject/SDNADecorators.ts rename to core/src/subject/decorators.ts From 5d852011edf47a927e2f98f2f35ab8fc233cc703 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Thu, 20 Mar 2025 23:52:23 +0100 Subject: [PATCH 02/15] rename SubjectEntity to Ad4mModel --- core/src/index.ts | 2 +- core/src/perspectives/PerspectiveClient.ts | 2 +- core/src/perspectives/PerspectiveProxy.ts | 2 +- .../{SubjectEntity.ts => Ad4mModel.ts} | 54 +++++++++---------- tests/js/tests/prolog-and-literals.test.ts | 18 +++---- 5 files changed, 37 insertions(+), 41 deletions(-) rename core/src/subject/{SubjectEntity.ts => Ad4mModel.ts} (95%) diff --git a/core/src/index.ts b/core/src/index.ts index 320905dca..99006524d 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -26,8 +26,8 @@ export * from "./neighbourhood/NeighbourhoodProxy"; export * from "./typeDefs"; export * from "./DID"; export * from "./utils"; -export * from './subject/SubjectEntity' export * from "./agent/AgentClient"; export * from "./ai/AIClient" export * from "./ai/Tasks" export * from "./runtime/RuntimeResolver" +export * from './subject/Ad4mModel' diff --git a/core/src/perspectives/PerspectiveClient.ts b/core/src/perspectives/PerspectiveClient.ts index 50f3ec65e..87a6f94d0 100644 --- a/core/src/perspectives/PerspectiveClient.ts +++ b/core/src/perspectives/PerspectiveClient.ts @@ -10,7 +10,7 @@ import { Perspective } from "./Perspective"; import { PerspectiveHandle, PerspectiveState } from "./PerspectiveHandle"; import { LinkStatus, PerspectiveProxy } from './PerspectiveProxy'; import { AIClient } from "../ai/AIClient"; -import { AllInstancesResult } from "../subject/SubjectEntity"; +import { AllInstancesResult } from "../subject/Ad4mModel"; const LINK_EXPRESSION_FIELDS = ` author diff --git a/core/src/perspectives/PerspectiveProxy.ts b/core/src/perspectives/PerspectiveProxy.ts index 3a6cf3cef..687369af2 100644 --- a/core/src/perspectives/PerspectiveProxy.ts +++ b/core/src/perspectives/PerspectiveProxy.ts @@ -12,7 +12,7 @@ import { NeighbourhoodExpression } from "../neighbourhood/Neighbourhood"; import { AIClient } from "../ai/AIClient"; import { PERSPECTIVE_QUERY_SUBSCRIPTION } from "./PerspectiveResolver"; import { gql } from "@apollo/client/core"; -import { AllInstancesResult } from "../subject/SubjectEntity"; +import { AllInstancesResult } from "../subject/Ad4mModel"; type QueryCallback = (result: AllInstancesResult) => void; diff --git a/core/src/subject/SubjectEntity.ts b/core/src/subject/Ad4mModel.ts similarity index 95% rename from core/src/subject/SubjectEntity.ts rename to core/src/subject/Ad4mModel.ts index a9e268134..d41387636 100644 --- a/core/src/subject/SubjectEntity.ts +++ b/core/src/subject/Ad4mModel.ts @@ -28,7 +28,7 @@ export type Query = { count?: boolean; }; -export type AllInstancesResult = { AllInstances: SubjectEntity[]; TotalCount?: number }; +export type AllInstancesResult = { AllInstances: Ad4mModel[]; TotalCount?: number }; export type ResultsWithTotalCount = { results: T[]; totalCount?: number }; export type PaginationResult = { results: T[]; totalCount?: number; pageSize: number; pageNumber: number }; @@ -44,7 +44,7 @@ function buildSourceQuery(source?: string): string { // todo: only return Timestamp & Author from query (Base, AllLinks, and SortLinks not required) function buildAuthorAndTimestampQuery(): string { - // Gets the author and timestamp of a SubjectEntity instance (based on the first link mentioning the base) + // Gets the author and timestamp of a Ad4mModel instance (based on the first link mentioning the base) return ` findall( [T, A], @@ -57,7 +57,7 @@ function buildAuthorAndTimestampQuery(): string { } function buildPropertiesQuery(properties?: string[]): string { - // Gets the name, value, and resolve boolean for all (or some) properties on a SubjectEntity instance + // Gets the name, value, and resolve boolean for all (or some) properties on a Ad4mModel instance // Resolves literals (if property_resolve/2 is true) to their value - either the data field if it is // an Expression in JSON literal, or the direct literal value if it is a simple literal // If no properties are provided, all are included @@ -71,7 +71,7 @@ function buildPropertiesQuery(properties?: string[]): string { } function buildCollectionsQuery(collections?: string[]): string { - // Gets the name and array of values for all (or some) collections on a SubjectEntity instance + // Gets the name and array of values for all (or some) collections on a Ad4mModel instance // If no collections are provided, all are included return ` findall([CollectionName, CollectionValues], ( @@ -178,7 +178,7 @@ function buildLimitQuery(limit?: number): string { * @example * ```typescript * @SDNAClass({ name: "Recipe" }) - * class Recipe extends SubjectEntity { + * class Recipe extends Ad4mModel { * @SubjectProperty({ * through: "recipe://name", * writable: true, @@ -188,7 +188,7 @@ function buildLimitQuery(limit?: number): string { * } * ``` */ -export class SubjectEntity { +export class Ad4mModel { #baseExpression: string; #subjectClassName: string; #source: string; @@ -196,7 +196,7 @@ export class SubjectEntity { author: string; timestamp: string; - private static classNamesByClass = new WeakMap(); + private static classNamesByClass = new WeakMap(); static async getClassName(perspective: PerspectiveProxy) { // Get or create the cache for this class @@ -254,7 +254,7 @@ export class SubjectEntity { public static async assignValuesToInstance( perspective: PerspectiveProxy, - instance: SubjectEntity, + instance: Ad4mModel, values: ValueTuple[] ) { // Map properties to object @@ -278,7 +278,7 @@ export class SubjectEntity { } private async getData() { - // Builds an object with the author, timestamp, all properties, & all collections on the SubjectEntity and saves it to the instance + // Builds an object with the author, timestamp, all properties, & all collections on the Ad4mModel and saves it to the instance const subQueries = [buildAuthorAndTimestampQuery(), buildPropertiesQuery(), buildCollectionsQuery()]; const fullQuery = ` Base = "${this.#baseExpression}", @@ -290,7 +290,7 @@ export class SubjectEntity { if (result?.[0]) { const { Properties, Collections, Timestamp, Author } = result?.[0]; const values = [...Properties, ...Collections, ["timestamp", Timestamp], ["author", Author]]; - await SubjectEntity.assignValuesToInstance(this.#perspective, this, values); + await Ad4mModel.assignValuesToInstance(this.#perspective, this, values); } return this; @@ -322,8 +322,8 @@ export class SubjectEntity { return fullQuery; } - public static async instancesFromPrologResult( - this: typeof SubjectEntity & (new (...args: any[]) => T), + public static async instancesFromPrologResult( + this: typeof Ad4mModel & (new (...args: any[]) => T), perspective: PerspectiveProxy, query: Query, result: AllInstancesResult @@ -343,7 +343,7 @@ export class SubjectEntity { } // Collect values to assign to instance const values = [...Properties, ...Collections, ["timestamp", Timestamp], ["author", Author]]; - await SubjectEntity.assignValuesToInstance(perspective, instance, values); + await Ad4mModel.assignValuesToInstance(perspective, instance, values); return instance; } catch (error) { @@ -357,11 +357,11 @@ export class SubjectEntity { } /** - * Gets all instances of the subject entity in the perspective that match the query params. + * Gets all instances of the model in the perspective that match the query params. * * @param perspective - The perspective to search in * @param query - Optional query parameters to filter results - * @returns Array of matching subject entities + * @returns Array of matching models * * @example * ```typescript @@ -379,8 +379,8 @@ export class SubjectEntity { * }); * ``` */ - static async findAll( - this: typeof SubjectEntity & (new (...args: any[]) => T), + static async findAll( + this: typeof Ad4mModel & (new (...args: any[]) => T), perspective: PerspectiveProxy, query: Query = {} ): Promise { @@ -406,8 +406,8 @@ export class SubjectEntity { * console.log(`Showing 10 of ${totalCount} dessert recipes`); * ``` */ - static async findAllAndCount( - this: typeof SubjectEntity & (new (...args: any[]) => T), + static async findAllAndCount( + this: typeof Ad4mModel & (new (...args: any[]) => T), perspective: PerspectiveProxy, query: Query = {} ): Promise> { @@ -433,8 +433,8 @@ export class SubjectEntity { * console.log(`Page ${page.pageNumber} of recipes, ${page.results.length} items`); * ``` */ - static async paginate( - this: typeof SubjectEntity & (new (...args: any[]) => T), + static async paginate( + this: typeof Ad4mModel & (new (...args: any[]) => T), perspective: PerspectiveProxy, pageSize: number, pageNumber: number, @@ -697,8 +697,8 @@ export class SubjectEntity { * }); * ``` */ - static query( - this: typeof SubjectEntity & (new (...args: any[]) => T), + static query( + this: typeof Ad4mModel & (new (...args: any[]) => T), perspective: PerspectiveProxy, query?: Query ): SubjectQueryBuilder { @@ -706,7 +706,7 @@ export class SubjectEntity { } } -/** Query builder for SubjectEntity queries. +/** Query builder for Ad4mModel queries. * Allows building queries with a fluent interface and either running them once * or subscribing to updates. * @@ -726,12 +726,12 @@ export class SubjectEntity { * }); * ``` */ -export class SubjectQueryBuilder { +export class SubjectQueryBuilder { private perspective: PerspectiveProxy; private queryParams: Query = {}; - private ctor: typeof SubjectEntity; + private ctor: typeof Ad4mModel; - constructor(perspective: PerspectiveProxy, ctor: typeof SubjectEntity, query?: Query) { + constructor(perspective: PerspectiveProxy, ctor: typeof Ad4mModel, query?: Query) { this.perspective = perspective; this.ctor = ctor; if (query) this.queryParams = query; diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index faa30e345..289f9f14b 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -5,7 +5,7 @@ import { Ad4mClient, Link, LinkQuery, Literal, PerspectiveProxy, InstanceQuery, Subject, SubjectProperty, SubjectCollection, SubjectFlag, SDNAClass, - SubjectEntity, + Ad4mModel, } from "@coasys/ad4m"; import { readFileSync } from "node:fs"; import { startExecutor, apolloClient } from "../utils/utils"; @@ -548,7 +548,7 @@ describe("Prolog + Literals", () => { @SDNAClass({ name: "Recipe" }) - class Recipe extends SubjectEntity { + class Recipe extends Ad4mModel { //@ts-ignore @SubjectFlag({ through: "ad4m://type", @@ -617,10 +617,6 @@ describe("Prolog + Literals", () => { resolveLanguage: "literal" }) resolve: string = "" - - // static query(perspective: PerspectiveProxy) { - // return SubjectEntity.query(perspective); - // } } before(async () => { @@ -1245,7 +1241,7 @@ describe("Prolog + Literals", () => { @SDNAClass({ name: "Task_due" }) - class TaskDue extends SubjectEntity { + class TaskDue extends Ad4mModel { @SubjectProperty({ through: "task://title", writable: true, @@ -1783,7 +1779,7 @@ describe("Prolog + Literals", () => { @SDNAClass({ name: "Notification" }) - class Notification extends SubjectEntity { + class Notification extends Ad4mModel { @SubjectProperty({ through: "notification://title", writable: true, @@ -1883,7 +1879,7 @@ describe("Prolog + Literals", () => { @SDNAClass({ name: "Note1" }) - class Note1 extends SubjectEntity { + class Note1 extends Ad4mModel { @SubjectProperty({ through: "note://name", writable: true, @@ -1906,7 +1902,7 @@ describe("Prolog + Literals", () => { @SDNAClass({ name: "Note2" }) - class Note2 extends SubjectEntity { + class Note2 extends Ad4mModel { @SubjectProperty({ through: "note://name", writable: true, @@ -1956,7 +1952,7 @@ describe("Prolog + Literals", () => { @SDNAClass({ name: "Task" }) - class Task extends SubjectEntity { + class Task extends Ad4mModel { @SubjectProperty({ through: "task://description", writable: true, From 8c6dbf68c221300d71323e12d7e04c01b5c3c073 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 00:23:16 +0100 Subject: [PATCH 03/15] rename decorator functions add some aliases --- core/src/subject/decorators.ts | 80 +++++++++-- tests/js/tests/prolog-and-literals.test.ts | 154 +++++++-------------- 2 files changed, 119 insertions(+), 115 deletions(-) diff --git a/core/src/subject/decorators.ts b/core/src/subject/decorators.ts index 278543f2e..728f6e698 100644 --- a/core/src/subject/decorators.ts +++ b/core/src/subject/decorators.ts @@ -138,6 +138,57 @@ export interface PropertyOptions { local?: boolean; } + +/** + * Decorator for defining required and writable properties of a model class. + * + * @category Decorators + * + * @description + * This will define a required property that must have an initial value. + * All the same options as Property are available, but required is automatically set to true. + * If no initial value is provided, it defaults to "literal://string:uninitialized" + * + * @example + * // Usage + * Property({ through: "ad4m://name" }) // defines a required property with default initial value + * Property({ through: "ad4m://name", initial: "John" }) // defines a required property with custom initial value + * + * @param {PropertyOptions} [opts] - Property options. + */ +export function Property(opts: PropertyOptions) { + return Optional({ + ...opts, + required: true, + initial: opts.initial || "literal://string:uninitialized" + }); +} + +/** + * Decorator for defining read-only properties of a subject class. + * + * @category Decorators + * + * @description + * This will define a read-only property that cannot be modified after initialization. + * All the same options as Property are available, but writable is automatically set to false. + * + * @example + * // Usage + * ReadOnly({ through: "ad4m://id" }) // defines a read-only property + * + * + * @param {PropertyOptions} [opts] - Property options. + */ +export function ReadOnly(opts: PropertyOptions) { + return Optional({ + ...opts, + writable: false + }); +} + + + /** * Decorator for defining properties of a subject class. * @@ -147,7 +198,7 @@ export interface PropertyOptions { * This will allow you to define properties with different conditions and how they would be defined in proflog engine. * * - All properties must have a `through` option which is the predicate of the property. - * -e If the property is required, it must have an `initial` option which is the initial value of the property. + * - If the property is required, it must have an `initial` option which is the initial value of the property. * - If the property is writable, it will have a setter in prolog engine. A custom setter can be defined with the `setter` option. * - If resolveLanguage is defined, you can use the default `Literal` Language or use your custom language address that can be used to store the property. * - If a custom getter is defined, it will be used to get the value of the property in prolog engine. If not, the default getter will be used. @@ -155,11 +206,11 @@ export interface PropertyOptions { * * @example * // Usage - * SubjectProperty({ through: "ad4m://name", initial: "John", required: true }) // this will define a property with the name "ad4m://name" and the initial value "John" + * Optional({ through: "ad4m://name", initial: "John", required: true }) // this will define a property with the name "ad4m://name" and the initial value "John" * * @param {PropertyOptions} [opts] - Property options. */ -export function SubjectProperty(opts: PropertyOptions) { +export function Optional(opts: PropertyOptions) { return function (target: T, key: keyof T) { if (opts.required && !opts.initial) { throw new Error("SubjectProperty requires an 'initial' option if 'required' is true"); @@ -195,7 +246,7 @@ export interface FlagOptions { } /** - * Decorator for defining flags of a subject class + * Decorator for defining flags on model classes. * * @category Decorators * @@ -209,11 +260,11 @@ export interface FlagOptions { * * @example * // Usage - * SubjectFlag({ through: "ad4m://name", value: "John" }) // this will define a flag with the name "ad4m://name" and the initial value "John" + * Flag({ through: "ad4m://name", value: "John" }) // this will define a flag with the name "ad4m://name" and the initial value "John" * * @param {FlagOptions} [opts] Flag options. */ -export function SubjectFlag(opts: FlagOptions) { +export function Flag(opts: FlagOptions) { return function (target: T, key: keyof T) { if (!opts.through && !opts.value) { throw new Error("SubjectFlag requires a 'through' and 'value' option") @@ -267,7 +318,7 @@ export interface CollectionOptions { } /** - * Decorator for defining collections of a subject class. + * Decorator for defining collections on model classes. * * @category Decorators * @@ -282,11 +333,11 @@ export interface CollectionOptions { * * @example * // Usage - * SubjectCollection({ through: "ad4m://friends" }) // this will define a collection with the name "ad4m://friends" + * Collection({ through: "ad4m://friends" }) // this will define a collection with the name "ad4m://friends" * * @param opts Collection options. */ -export function SubjectCollection(opts: CollectionOptions) { +export function Collection(opts: CollectionOptions) { return function (target: T, key: keyof T) { target["__collections"] = target["__collections"] || {}; target["__collections"][key] = opts; @@ -310,7 +361,7 @@ export function makeRandomPrologAtom(length: number): string { return result; } -export interface SDNAClassOptions { +export interface ModelOptionsOptions { /** * The name of the entity. */ @@ -318,7 +369,8 @@ export interface SDNAClassOptions { } /** - * Decorator for defining an SDNA class. + * Decorator for defining options on a model class to be stored as + * a Social DNA subject class in AD4M. * * @category Decorators * @@ -329,11 +381,11 @@ export interface SDNAClassOptions { * * @example * // Usage - * SDNAClass({ name: "Person" }) // this will create a new SDNA class with the name "Person" + * ModelOptions({ name: "Person" }) // this will create a new SDNA class with the name "Person" * - * @param opts SDNA class options. + * @param opts Model options. */ -export function SDNAClass(opts: SDNAClassOptions) { +export function ModelOptions(opts: ModelOptionsOptions) { return function (target: any) { target.prototype.className = opts.name; target.className = opts.name; diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index 289f9f14b..725c4fccf 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -2,15 +2,17 @@ import { expect } from "chai"; import { ChildProcess } from 'node:child_process'; import { Ad4mClient, Link, LinkQuery, Literal, PerspectiveProxy, SmartLiteral, SMART_LITERAL_CONTENT_PREDICATE, - InstanceQuery, Subject, SubjectProperty, - SubjectCollection, SubjectFlag, - SDNAClass, + InstanceQuery, Subject, Ad4mModel, + Flag, + Property, + ReadOnly, + Collection, + ModelOptions, } from "@coasys/ad4m"; import { readFileSync } from "node:fs"; import { startExecutor, apolloClient } from "../utils/utils"; import path from "path"; -import fs from "fs"; import { fileURLToPath } from 'url'; import fetch from 'node-fetch' @@ -258,12 +260,12 @@ describe("Prolog + Literals", () => { }) describe("SDNA creation decorators", () => { - @SDNAClass({ + @ModelOptions({ name: "Message" }) class Message { //@ts-ignore - @SubjectFlag({ + @Flag({ through: "ad4m://type", value: "ad4m://message" }) @@ -274,17 +276,16 @@ describe("Prolog + Literals", () => { static async all(perspective: PerspectiveProxy): Promise { return [] } //@ts-ignore - @SubjectProperty({ + @Property({ through: "todo://state", initial: "todo://ready", - writable: true, }) body: string = "" } // This class matches the SDNA in ./sdna/subject.pl // and this test proves the decorators create the exact same SDNA code - @SDNAClass({ + @ModelOptions({ name: "Todo" }) class Todo { @@ -316,45 +317,37 @@ describe("Prolog + Literals", () => { static async allSelf(perspective: PerspectiveProxy): Promise { return [] } //@ts-ignore - @SubjectProperty({ + @Property({ through: "todo://state", - initial:"todo://ready", - writable: true, - required: true + initial: "todo://ready", }) state: string = "" - //@ts-ignore - @SubjectProperty({ + @Property({ through: "todo://has_title", writable: true, resolveLanguage: "literal" }) title: string = "" - @SubjectProperty({ + @ReadOnly({ getter: `triple(Base, "flux://has_reaction", "flux://thumbsup"), Value = true` }) isLiked: boolean = false - //@ts-ignore - @SubjectCollection({ through: "todo://comment" }) - // @ts-ignore + @Collection({ through: "todo://comment" }) comments: string[] = [] - //@ts-ignore - @SubjectCollection({ through: "flux://entry_type" }) + @Collection({ through: "flux://entry_type" }) entries: string[] = [] - //@ts-ignore - @SubjectCollection({ + @Collection({ through: "flux://entry_type", where: { isInstance: Message } }) messages: string[] = [] - //@ts-ignore - @SubjectCollection({ + @Collection({ through: "flux://entry_type", where: { condition: `triple(Target, "flux://has_reaction", "flux://thumbsup")` } }) @@ -444,11 +437,13 @@ describe("Prolog + Literals", () => { it("can easily be initialized with PerspectiveProxy.ensureSDNASubjectClass()", async () => { expect(await perspective!.getSdna()).to.have.lengthOf(1) - @SDNAClass({ + @ModelOptions({ name: "Test" }) class Test { - @SubjectProperty({through: "test://test_numer"}) + @Property({ + through: "test://test_numer" + }) number: number = 0 } @@ -545,75 +540,59 @@ describe("Prolog + Literals", () => { }) describe("Active record implementation", () => { - @SDNAClass({ + @ModelOptions({ name: "Recipe" }) class Recipe extends Ad4mModel { - //@ts-ignore - @SubjectFlag({ + @Flag({ through: "ad4m://type", value: "ad4m://recipe" }) type: string = "" - //@ts-ignore - @SubjectProperty({ + @Property({ through: "recipe://plain", - writable: true, }) plain: string = "" - //@ts-ignore - @SubjectProperty({ + @Property({ through: "recipe://name", - writable: true, resolveLanguage: "literal" }) name: string = "" - // @ts-ignore - @SubjectProperty({ + @Property({ through: "recipe://boolean", - writable: true, resolveLanguage: "literal" }) booleanTest: boolean = false - @SubjectProperty({ + @Property({ through: "recipe://number", - writable: true, resolveLanguage: "literal" }) number: number = 0 - //@ts-ignore - @SubjectCollection({ through: "recipe://entries" }) + @Collection({ through: "recipe://entries" }) entries: string[] = [] - // @ts-ignore - @SubjectCollection({ + @Collection({ through: "recipe://entries", where: { condition: `triple(Target, "recipe://has_ingredient", "recipe://test")` } }) - // @ts-ignore - ingredients: []; + ingredients: string[] = [] - //@ts-ignore - @SubjectCollection({ through: "recipe://comment" }) - // @ts-ignore + @Collection({ through: "recipe://comment" }) comments: string[] = [] - //@ts-ignore - @SubjectProperty({ + @Property({ through: "recipe://local", - writable: true, local: true }) local: string = "" - @SubjectProperty({ + @Property({ through: "recipe://resolve", - writable: true, resolveLanguage: "literal" }) resolve: string = "" @@ -1238,29 +1217,25 @@ describe("Prolog + Literals", () => { }) it("findAll() works with where query between operations", async () => { - @SDNAClass({ + @ModelOptions({ name: "Task_due" }) class TaskDue extends Ad4mModel { - @SubjectProperty({ + @Property({ through: "task://title", - writable: true, - required: true, - initial: "task://notitle", resolveLanguage: "literal" }) title: string = ""; - @SubjectProperty({ + @Property({ through: "task://priority", writable: true, resolveLanguage: "literal" }) priority: number = 0; - @SubjectProperty({ + @Property({ through: "task://dueDate", - writable: true, resolveLanguage: "literal" }) dueDate: number = 0; @@ -1776,29 +1751,24 @@ describe("Prolog + Literals", () => { }) it("query builder works with subscriptions", async () => { - @SDNAClass({ + @ModelOptions({ name: "Notification" }) class Notification extends Ad4mModel { - @SubjectProperty({ + @Property({ through: "notification://title", - writable: true, - required: true, - initial: "literal://string:notitle", resolveLanguage: "literal" }) title: string = ""; - @SubjectProperty({ + @Property({ through: "notification://priority", - writable: true, resolveLanguage: "literal" }) priority: number = 0; - @SubjectProperty({ + @Property({ through: "notification://read", - writable: true, resolveLanguage: "literal" }) read: boolean = false; @@ -1876,47 +1846,35 @@ describe("Prolog + Literals", () => { it("query builder should filter by subject class", async () => { // Define a second subject class - @SDNAClass({ + @ModelOptions({ name: "Note1" }) class Note1 extends Ad4mModel { - @SubjectProperty({ + @Property({ through: "note://name", - writable: true, - required: true, - initial: "note://noname", resolveLanguage: "literal" }) name: string = ""; - @SubjectProperty({ + @Property({ through: "note1://content", - writable: true, - required: true, - initial: "note1://nocontent", resolveLanguage: "literal" }) content1: string = ""; } - @SDNAClass({ + @ModelOptions({ name: "Note2" }) class Note2 extends Ad4mModel { - @SubjectProperty({ + @Property({ through: "note://name", - writable: true, - required: true, - initial: "note://noname", resolveLanguage: "literal" }) name: string = ""; - @SubjectProperty({ + @Property({ through: "note2://content", - writable: true, - required: true, - initial: "note2://nocontent", resolveLanguage: "literal" }) content2: string = ""; @@ -1949,37 +1907,31 @@ describe("Prolog + Literals", () => { }); it("query builder works with single query object, complex query and subscriptions", async () => { - @SDNAClass({ + @ModelOptions({ name: "Task" }) class Task extends Ad4mModel { - @SubjectProperty({ + @Property({ through: "task://description", - writable: true, - required: true, - initial: "task://nodescription", resolveLanguage: "literal" }) description: string = ""; - @SubjectProperty({ + @Property({ through: "task://dueDate", - writable: true, resolveLanguage: "literal" }) dueDate: number = 0; - @SubjectProperty({ + @Property({ through: "task://completed", - writable: true, resolveLanguage: "literal" }) completed: boolean = false; - @SubjectProperty({ + @Property({ through: "task://assignee", - writable: true, resolveLanguage: "literal" }) assignee: string = ""; From 03724fbf19ec9a469f762c8cdcff26355cbeb83c Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:29:41 +0100 Subject: [PATCH 04/15] Update scryer rev to commit without overwrite warning --- Cargo.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2e44fa53..8cf43b998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1278,7 +1278,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.10.5", "lazy_static", "lazycell", "log", @@ -3003,7 +3003,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.11", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -3614,8 +3614,8 @@ source = "git+https://github.com/coasys/deno_core.git?branch=v8-dylib#4c825881bc dependencies = [ "anyhow", "bincode", - "bit-set 0.8.0", - "bit-vec 0.8.0", + "bit-set 0.5.3", + "bit-vec 0.6.3", "bytes", "cooked-waker", "deno_core_icudata", @@ -7980,7 +7980,7 @@ dependencies = [ "httpdate", "itoa 1.0.11", "pin-project-lite", - "socket2 0.5.7", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -12290,7 +12290,7 @@ dependencies = [ "font", "itertools 0.13.0", "log", - "ordered-float 4.2.2", + "ordered-float 2.10.1", "pathfinder_color", "pathfinder_content", "pathfinder_geometry", @@ -14878,7 +14878,7 @@ dependencies = [ [[package]] name = "scryer-prolog" version = "0.9.4" -source = "git+https://github.com/coasys/scryer-prolog#ea02b54e7fc164f026e4f52702088d5b0146c2a3" +source = "git+https://github.com/coasys/scryer-prolog#badef57ebfc95acc85681309d0cd9e5bfde82f4f" dependencies = [ "arcu", "base64 0.22.1", @@ -15719,7 +15719,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.87", @@ -18294,7 +18294,7 @@ version = "1.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ - "cfg-if 1.0.0", + "cfg-if 0.1.10", "rand 0.6.5", "static_assertions", ] From 5261d61573d466bd0f44eef014dd5cfeeead3809 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:36:06 +0100 Subject: [PATCH 05/15] =?UTF-8?q?Don=E2=80=99t=20spam=20log=20with=20subje?= =?UTF-8?q?ct=20instance=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/perspectives/perspective_instance.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rust-executor/src/perspectives/perspective_instance.rs b/rust-executor/src/perspectives/perspective_instance.rs index 7d28765eb..3f137f249 100644 --- a/rust-executor/src/perspectives/perspective_instance.rs +++ b/rust-executor/src/perspectives/perspective_instance.rs @@ -1619,18 +1619,18 @@ impl PerspectiveInstance { Ok(QueryResolution::True) => instance_check_passed = true, Ok(QueryResolution::Matches(_)) => instance_check_passed = true, Err(e) => log::warn!("Error trying to check instance after create_subject: {}", e), - Ok(r) => log::info!("create_subject instance query returned: {:?}", r), + Ok(r) => {}//log::info!("create_subject instance query returned: {:?}", r), } sleep(Duration::from_millis(10)).await; tries += 1; } if instance_check_passed { - log::info!( - "Subject class \"{}\" successfully instantiated around \"{}\".", - class_name, - expression_address - ); + // log::info!( + // "Subject class \"{}\" successfully instantiated around \"{}\".", + // class_name, + // expression_address + // ); } else { log::warn!("create_subject: instance check still false after running constructor and waiting 5s. Something seems off with subject class: {}", class_name); } From 36917291f69c58a13afe2e07cf8eccbf228fde86 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:36:29 +0100 Subject: [PATCH 06/15] @Property always writable --- core/src/subject/decorators.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/subject/decorators.ts b/core/src/subject/decorators.ts index 728f6e698..439688eb0 100644 --- a/core/src/subject/decorators.ts +++ b/core/src/subject/decorators.ts @@ -160,6 +160,7 @@ export function Property(opts: PropertyOptions) { return Optional({ ...opts, required: true, + writable: true, initial: opts.initial || "literal://string:uninitialized" }); } From 11c61d5dc23e92e106ec5fbef370bb84fdc812f5 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:38:52 +0100 Subject: [PATCH 07/15] =?UTF-8?q?warning=E2=80=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust-executor/src/perspectives/perspective_instance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-executor/src/perspectives/perspective_instance.rs b/rust-executor/src/perspectives/perspective_instance.rs index 3f137f249..7579798f0 100644 --- a/rust-executor/src/perspectives/perspective_instance.rs +++ b/rust-executor/src/perspectives/perspective_instance.rs @@ -1619,7 +1619,7 @@ impl PerspectiveInstance { Ok(QueryResolution::True) => instance_check_passed = true, Ok(QueryResolution::Matches(_)) => instance_check_passed = true, Err(e) => log::warn!("Error trying to check instance after create_subject: {}", e), - Ok(r) => {}//log::info!("create_subject instance query returned: {:?}", r), + Ok(_) => {} //log::info!("create_subject instance query returned: {:?}", r), } sleep(Duration::from_millis(10)).await; tries += 1; From 6635aa373c1f7d33e3f19a738fd307dad8810018 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:52:25 +0100 Subject: [PATCH 08/15] Property decorators default to writable: true --- core/src/subject/decorators.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/subject/decorators.ts b/core/src/subject/decorators.ts index 439688eb0..d9446251c 100644 --- a/core/src/subject/decorators.ts +++ b/core/src/subject/decorators.ts @@ -213,6 +213,10 @@ export function ReadOnly(opts: PropertyOptions) { */ export function Optional(opts: PropertyOptions) { return function (target: T, key: keyof T) { + if(typeof opts.writable === "undefined") { + opts.writable = true + } + if (opts.required && !opts.initial) { throw new Error("SubjectProperty requires an 'initial' option if 'required' is true"); } From 2efd43d89f3caa8edcd8dd57e55a3bf02fb91d7e Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:52:49 +0100 Subject: [PATCH 09/15] Use correct decorator as to not change test assertions --- tests/js/tests/prolog-and-literals.test.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index 725c4fccf..bb5b5f7c2 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -9,6 +9,7 @@ import { Ad4mClient, Link, LinkQuery, Literal, PerspectiveProxy, ReadOnly, Collection, ModelOptions, + Optional, } from "@coasys/ad4m"; import { readFileSync } from "node:fs"; import { startExecutor, apolloClient } from "../utils/utils"; @@ -264,19 +265,16 @@ describe("Prolog + Literals", () => { name: "Message" }) class Message { - //@ts-ignore @Flag({ through: "ad4m://type", value: "ad4m://message" }) type: string = "" - //@ts-ignore @InstanceQuery() static async all(perspective: PerspectiveProxy): Promise { return [] } - //@ts-ignore - @Property({ + @Optional({ through: "todo://state", initial: "todo://ready", }) @@ -323,7 +321,7 @@ describe("Prolog + Literals", () => { }) state: string = "" - @Property({ + @Optional({ through: "todo://has_title", writable: true, resolveLanguage: "literal" @@ -550,24 +548,24 @@ describe("Prolog + Literals", () => { }) type: string = "" - @Property({ + @Optional({ through: "recipe://plain", }) plain: string = "" - @Property({ + @Optional({ through: "recipe://name", resolveLanguage: "literal" }) name: string = "" - @Property({ + @Optional({ through: "recipe://boolean", resolveLanguage: "literal" }) booleanTest: boolean = false - @Property({ + @Optional({ through: "recipe://number", resolveLanguage: "literal" }) @@ -585,13 +583,13 @@ describe("Prolog + Literals", () => { @Collection({ through: "recipe://comment" }) comments: string[] = [] - @Property({ + @Optional({ through: "recipe://local", local: true }) local: string = "" - @Property({ + @Optional({ through: "recipe://resolve", resolveLanguage: "literal" }) From ed82c190759a65c1709ab6ae1dd7421fa8f1dabf Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 14:59:34 +0100 Subject: [PATCH 10/15] =?UTF-8?q?debug=20logs=E2=80=94=20and=20retry=20loo?= =?UTF-8?q?p=20for=20pagination=20subscription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/js/tests/prolog-and-literals.test.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index bb5b5f7c2..8a21a9e4b 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -67,7 +67,7 @@ describe("Prolog + Literals", () => { before(async () => { perspective = await ad4m!.perspective.add("test") // for test debugging: - console.log("UUID: " + perspective.uuid) + //console.log("UUID: " + perspective.uuid) let classes = await perspective.subjectClasses(); expect(classes.length).to.equal(0) @@ -1554,7 +1554,7 @@ describe("Prolog + Literals", () => { // Test count with offset, limit, & where equality constraint (exists) const { results: recipes4, totalCount: count4 } = await Recipe.findAllAndCount(perspective!, { where: { number: 3 }, offset: 3, limit: 3, count: true }); - console.log('recipes4: ', recipes4); + //console.log('recipes4: ', recipes4); expect(recipes4.length).to.equal(0); expect(count4).to.equal(0); @@ -1738,7 +1738,14 @@ describe("Prolog + Literals", () => { newRecipe.name = "Recipe 11"; await newRecipe.save(); - await sleep(2000); + let tries = 0; + while (!lastResult) { + await sleep(500); + tries++; + if (tries > 20) { + throw new Error("Timeout waiting for subscription to update"); + } + } expect(lastResult.totalCount).to.equal(11); // Clean up @@ -1894,7 +1901,7 @@ describe("Prolog + Literals", () => { // Query for recipes - this should only return the recipe instance const note1Results = await Note1.query(perspective!).where({ name: "Test Item" }).get() - console.log("note1Results: ", note1Results) + //console.log("note1Results: ", note1Results) // This assertion will fail because the query builder doesn't filter by class expect(note1Results.length).to.equal(1); expect(note1Results[0]).to.be.instanceOf(Note1); @@ -2030,7 +2037,7 @@ describe("Prolog + Literals", () => { before(async () => { perspective = await ad4m!.perspective.add("smart literal test") // for test debugging: - console.log("UUID: " + perspective.uuid) + //console.log("UUID: " + perspective.uuid) }) it("can create and use a new smart literal", async () => { From 9a0d72db0ce6dfe0e5e83f9fe02a40969f98e373 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 15:12:33 +0100 Subject: [PATCH 11/15] Decorator doc-comments --- core/src/subject/decorators.ts | 466 ++++++++++++++++++++++++++------- 1 file changed, 368 insertions(+), 98 deletions(-) diff --git a/core/src/subject/decorators.ts b/core/src/subject/decorators.ts index d9446251c..caddc5589 100644 --- a/core/src/subject/decorators.ts +++ b/core/src/subject/decorators.ts @@ -35,23 +35,44 @@ condition?: string; } /** - * Decorator for querying instances of a subject class. + * Decorator for querying instances of a model class. * * @category Decorators * * @description - * NOTE: Only works on methods that return a promise and will throw an error if not used on a method that returns a promise. - * This will allow you to query for instances of a subject class with custom clauses within the instance clauses. + * Allows you to define static query methods on your model class to retrieve instances based on custom conditions. + * This decorator can only be applied to static async methods that return a Promise of an array of model instances. * - * @example - * // Usage with where clause - * InstanceQuery({ where: { name: "John" }}) // this will return all the properties of the subject class with the name "John" + * The query can be constrained using either: + * - A `where` clause that matches property values + * - A custom Prolog `condition` for more complex queries * * @example - * // Usage with condition clause - * InstanceQuery({ condition: "triple(Instance, 'age', Age), Age > 18" }) // this will return all the properties of the subject class with the age greater than 18 - * - * @param {Object} [options] - Query options. + * ```typescript + * class Recipe extends Ad4mModel { + * @Property({ through: "recipe://name" }) + * name: string = ""; + * + * @Property({ through: "recipe://rating" }) + * rating: number = 0; + * + * // Get all recipes + * @InstanceQuery() + * static async all(perspective: PerspectiveProxy): Promise { return [] } + * + * // Get recipes by name + * @InstanceQuery({ where: { name: "Chocolate Cake" }}) + * static async findByName(perspective: PerspectiveProxy): Promise { return [] } + * + * // Get highly rated recipes using a custom condition + * @InstanceQuery({ condition: "triple(Instance, 'recipe://rating', Rating), Rating > 4" }) + * static async topRated(perspective: PerspectiveProxy): Promise { return [] } + * } + * ``` + * + * @param {Object} [options] - Query options + * @param {object} [options.where] - Object with property-value pairs to match + * @param {string} [options.condition] - Custom Prolog condition for more complex queries */ export function InstanceQuery(options?: InstanceQueryParams) { return function (target: T, key: keyof T, descriptor: PropertyDescriptor) { @@ -140,76 +161,83 @@ export interface PropertyOptions { /** - * Decorator for defining required and writable properties of a model class. - * - * @category Decorators - * - * @description - * This will define a required property that must have an initial value. - * All the same options as Property are available, but required is automatically set to true. - * If no initial value is provided, it defaults to "literal://string:uninitialized" - * - * @example - * // Usage - * Property({ through: "ad4m://name" }) // defines a required property with default initial value - * Property({ through: "ad4m://name", initial: "John" }) // defines a required property with custom initial value - * - * @param {PropertyOptions} [opts] - Property options. - */ -export function Property(opts: PropertyOptions) { - return Optional({ - ...opts, - required: true, - writable: true, - initial: opts.initial || "literal://string:uninitialized" - }); -} - -/** - * Decorator for defining read-only properties of a subject class. - * - * @category Decorators - * - * @description - * This will define a read-only property that cannot be modified after initialization. - * All the same options as Property are available, but writable is automatically set to false. - * - * @example - * // Usage - * ReadOnly({ through: "ad4m://id" }) // defines a read-only property - * - * - * @param {PropertyOptions} [opts] - Property options. - */ -export function ReadOnly(opts: PropertyOptions) { - return Optional({ - ...opts, - writable: false - }); -} - - - -/** - * Decorator for defining properties of a subject class. + * Decorator for defining optional properties on model classes. * * @category Decorators * * @description - * This will allow you to define properties with different conditions and how they would be defined in proflog engine. + * The most flexible property decorator that allows you to define properties with full control over: + * - Whether the property is required + * - Whether the property is writable + * - How values are stored and retrieved + * - Custom getter/setter logic + * - Local vs network storage * - * - All properties must have a `through` option which is the predicate of the property. - * - If the property is required, it must have an `initial` option which is the initial value of the property. - * - If the property is writable, it will have a setter in prolog engine. A custom setter can be defined with the `setter` option. - * - If resolveLanguage is defined, you can use the default `Literal` Language or use your custom language address that can be used to store the property. - * - If a custom getter is defined, it will be used to get the value of the property in prolog engine. If not, the default getter will be used. - * - If local is defined, the property will be stored locally in the perspective and not in the network. This is useful for properties that are not meant to be shared with the network + * Both @Property and @ReadOnly are specialized versions of @Optional with preset configurations. * * @example - * // Usage - * Optional({ through: "ad4m://name", initial: "John", required: true }) // this will define a property with the name "ad4m://name" and the initial value "John" - * - * @param {PropertyOptions} [opts] - Property options. + * ```typescript + * class Recipe extends Ad4mModel { + * // Basic optional property + * @Optional({ + * through: "recipe://description" + * }) + * description?: string; + * + * // Optional property with custom initial value + * @Optional({ + * through: "recipe://status", + * initial: "recipe://draft", + * required: true + * }) + * status: string = ""; + * + * // Read-only property with custom getter + * @Optional({ + * through: "recipe://rating", + * writable: false, + * getter: ` + * findall(Rating, triple(Base, "recipe://user_rating", Rating), Ratings), + * sum_list(Ratings, Sum), + * length(Ratings, Count), + * Value is Sum / Count + * ` + * }) + * averageRating: number = 0; + * + * // Property that resolves to a Literal and is stored locally + * @Optional({ + * through: "recipe://notes", + * resolveLanguage: "literal", + * local: true + * }) + * notes?: string; + * + * // Property with custom getter and setter logic + * @Optional({ + * through: "recipe://ingredients", + * getter: ` + * triple(Base, "recipe://ingredients", RawValue), + * atom_json_term(RawValue, Value) + * `, + * setter: ` + * atom_json_term(Value, JsonValue), + * Actions = [{"action": "setSingleTarget", "source": "this", "predicate": "recipe://ingredients", "target": JsonValue}] + * ` + * }) + * ingredients: string[] = []; + * } + * ``` + * + * @param {PropertyOptions} opts - Property configuration options + * @param {string} opts.through - The predicate URI for the property + * @param {string} [opts.initial] - Initial value (required if property is required) + * @param {boolean} [opts.required] - Whether the property must have a value + * @param {boolean} [opts.writable=true] - Whether the property can be modified + * @param {string} [opts.resolveLanguage] - Language to use for value resolution (e.g. "literal") + * @param {string} [opts.getter] - Custom Prolog code for getting the property value + * @param {string} [opts.setter] - Custom Prolog code for setting the property value + * @param {boolean} [opts.local] - Whether the property should only be stored locally */ export function Optional(opts: PropertyOptions) { return function (target: T, key: keyof T) { @@ -256,18 +284,52 @@ export interface FlagOptions { * @category Decorators * * @description - * The idea behind flag decorator is to define a property that is required and has an initial value. - * This will allow you to define a strict instance query. This behaviour can also be achieved with the `SubjectProperty` decorator but the `SubjectFlag` decorator is a shorthand for that. + * A specialized property decorator for defining immutable type flags or markers on model instances. + * Flags are always required properties with a fixed value that cannot be changed after creation. * - * NOTE: Use of Flag is discouraged and should be used only when necessary. + * Common uses for flags: + * - Type discrimination between different kinds of models + * - Marking models with specific capabilities or features + * - Versioning or compatibility markers * - * - All properties must have a `through` & `initial` option which is the predicate of the property. + * Note: Use of Flag is discouraged unless you specifically need type-based filtering or + * discrimination between different kinds of models. For most cases, regular properties + * with @Property or @Optional are more appropriate. * * @example - * // Usage - * Flag({ through: "ad4m://name", value: "John" }) // this will define a flag with the name "ad4m://name" and the initial value "John" - * - * @param {FlagOptions} [opts] Flag options. + * ```typescript + * class Message extends Ad4mModel { + * // Type flag to identify message models + * @Flag({ + * through: "ad4m://type", + * value: "ad4m://message" + * }) + * type: string = ""; + * + * // Version flag for compatibility + * @Flag({ + * through: "ad4m://version", + * value: "1.0.0" + * }) + * version: string = ""; + * + * // Feature flag + * @Flag({ + * through: "message://feature", + * value: "message://encrypted" + * }) + * feature: string = ""; + * } + * + * // Later you can query for specific types: + * const messages = await Message.query(perspective) + * .where({ type: "ad4m://message" }) + * .run(); + * ``` + * + * @param {FlagOptions} opts - Flag configuration + * @param {string} opts.through - The predicate URI for the flag + * @param {string} opts.value - The fixed value for the flag */ export function Flag(opts: FlagOptions) { return function (target: T, key: keyof T) { @@ -328,19 +390,64 @@ export interface CollectionOptions { * @category Decorators * * @description - * This will allow you to define collections with different conditions and how they would be defined in proflog engine. + * Defines a property that represents a collection of values linked to the model instance. + * Collections are always arrays and support operations for adding, removing, and setting values. * - * NOTE: The property needs to be an array for it to picked up during the initialization phase. + * For each collection property, the following methods are automatically generated: + * - `addX(value)` - Add a value to the collection + * - `removeX(value)` - Remove a value from the collection + * - `setCollectionX(values)` - Replace all values in the collection * - * - All collections must have a `through` option which is the predicate of the collection. - * - If the collection has a `where` option, it can be used to define a custom condition for the collection. - * - If the collection has a `local` option, the collection will be stored locally in the perspective and not in the network. This is useful for collections that are not meant to be shared with the network. + * Where X is the capitalized property name. * - * @example - * // Usage - * Collection({ through: "ad4m://friends" }) // this will define a collection with the name "ad4m://friends" + * Collections can be filtered using the `where` option to only include values that: + * - Are instances of a specific model class + * - Match a custom Prolog condition * - * @param opts Collection options. + * @example + * ```typescript + * class Recipe extends Ad4mModel { + * // Basic collection of ingredients + * @Collection({ + * through: "recipe://ingredient" + * }) + * ingredients: string[] = []; + * + * // Collection that only includes instances of another model + * @Collection({ + * through: "recipe://comment", + * where: { isInstance: Comment } + * }) + * comments: string[] = []; + * + * // Collection with custom filter condition + * @Collection({ + * through: "recipe://step", + * where: { condition: `triple(Target, "step://order", Order), Order < 3` } + * }) + * firstSteps: string[] = []; + * + * // Local-only collection not shared with network + * @Collection({ + * through: "recipe://note", + * local: true + * }) + * privateNotes: string[] = []; + * } + * + * // Using the generated methods: + * const recipe = new Recipe(perspective); + * await recipe.addIngredients("ingredient://flour"); + * await recipe.removeIngredients("ingredient://sugar"); + * await recipe.setCollectionIngredients(["ingredient://butter", "ingredient://eggs"]); + * ``` + * + * @param {CollectionOptions} opts - Collection configuration + * @param {string} opts.through - The predicate URI for collection links + * @param {WhereOptions} [opts.where] - Filter conditions for collection values + * @param {any} [opts.where.isInstance] - Model class to filter instances by + * @param {string} [opts.where.condition] - Custom Prolog condition for filtering + * @param {boolean} [opts.local] - Whether collection links are stored locally only */ export function Collection(opts: CollectionOptions) { return function (target: T, key: keyof T) { @@ -374,21 +481,58 @@ export interface ModelOptionsOptions { } /** - * Decorator for defining options on a model class to be stored as - * a Social DNA subject class in AD4M. + * Decorator for defining model classes in AD4M. * * @category Decorators * * @description - * This will create a new SDNA class with the given name and add custom methods to generate the SDNA for the class, for this to work the class need to have the properties defined using the decorators like `SubjectProperty`. + * The root decorator that must be applied to any class that represents a model in AD4M. + * It registers the class as a Social DNA (SDNA) subject class and provides the infrastructure + * for storing and retrieving instances. * - * Note: This decorator is required for the class to be picked up during the initialization phase. + * This decorator: + * - Registers the class with a unique name in the AD4M system + * - Generates the necessary SDNA code for the model's properties and collections + * - Enables the use of other model decorators (@Property, @Collection, etc.) + * - Provides static query methods through the Ad4mModel base class * * @example - * // Usage - * ModelOptions({ name: "Person" }) // this will create a new SDNA class with the name "Person" - * - * @param opts Model options. + * ```typescript + * @ModelOptions({ name: "Recipe" }) + * class Recipe extends Ad4mModel { + * @Property({ + * through: "recipe://name", + * resolveLanguage: "literal" + * }) + * name: string = ""; + * + * @Collection({ through: "recipe://ingredient" }) + * ingredients: string[] = []; + * + * // Static query methods from Ad4mModel: + * static async findByName(perspective: PerspectiveProxy, name: string) { + * return Recipe.query(perspective) + * .where({ name }) + * .run(); + * } + * } + * + * // Using the model: + * const recipe = new Recipe(perspective); + * recipe.name = "Chocolate Cake"; + * await recipe.save(); + * + * // Querying instances: + * const recipes = await Recipe.query(perspective) + * .where({ name: "Chocolate Cake" }) + * .run(); + * + * // Using with PerspectiveProxy: + * await perspective.ensureSDNASubjectClass(Recipe); + * ``` + * + * @param {ModelOptionsOptions} opts - Model configuration + * @param {string} opts.name - Unique name for the model class in AD4M */ export function ModelOptions(opts: ModelOptionsOptions) { return function (target: any) { @@ -565,4 +709,130 @@ export function ModelOptions(opts: ModelOptionsOptions) { Object.defineProperty(target, 'type', {configurable: true}); } +} + +/** + * Decorator for defining required and writable properties on model classes. + * + * @category Decorators + * + * @description + * A convenience decorator that defines a required property that must have an initial value and is writable by default. + * This is equivalent to using @Optional with `required: true` and `writable: true`. + * + * Properties defined with this decorator: + * - Must have a value (required) + * - Can be modified after creation (writable) + * - Default to "literal://string:uninitialized" if no initial value is provided + * + * @example + * ```typescript + * class User extends Ad4mModel { + * // Basic required property with default initial value + * @Property({ + * through: "user://name" + * }) + * name: string = ""; + * + * // Required property with custom initial value + * @Property({ + * through: "user://status", + * initial: "user://active" + * }) + * status: string = ""; + * + * // Required property with literal resolution + * @Property({ + * through: "user://bio", + * resolveLanguage: "literal" + * }) + * bio: string = ""; + * + * // Required property with custom getter/setter + * @Property({ + * through: "user://age", + * getter: `triple(Base, "user://birthYear", Year), Value is 2024 - Year`, + * setter: `Year is 2024 - Value, Actions = [{"action": "setSingleTarget", "source": "this", "predicate": "user://birthYear", "target": Year}]` + * }) + * age: number = 0; + * } + * ``` + * + * @param {PropertyOptions} opts - Property configuration + * @param {string} opts.through - The predicate URI for the property + * @param {string} [opts.initial] - Initial value (defaults to "literal://string:uninitialized") + * @param {string} [opts.resolveLanguage] - Language to use for value resolution (e.g. "literal") + * @param {string} [opts.getter] - Custom Prolog code for getting the property value + * @param {string} [opts.setter] - Custom Prolog code for setting the property value + * @param {boolean} [opts.local] - Whether the property should only be stored locally + */ +export function Property(opts: PropertyOptions) { + return Optional({ + ...opts, + required: true, + writable: true, + initial: opts.initial || "literal://string:uninitialized" + }); +} + +/** + * Decorator for defining read-only properties on model classes. + * + * @category Decorators + * + * @description + * A convenience decorator that defines a property that can only be read and cannot be modified after initialization. + * This is equivalent to using @Optional with `writable: false`. + * + * Read-only properties are ideal for: + * - Computed or derived values + * - Properties that should never change after creation + * - Properties that are set by the system + * - Properties that represent immutable data + * + * @example + * ```typescript + * class Post extends Ad4mModel { + * // Read-only property with custom getter for computed value + * @ReadOnly({ + * through: "post://likes", + * getter: `findall(User, triple(Base, "post://liked_by", User), Users), length(Users, Value)` + * }) + * likeCount: number = 0; + * + * // Read-only property for creation timestamp + * @ReadOnly({ + * through: "post://created_at", + * initial: new Date().toISOString() + * }) + * createdAt: string = ""; + * + * // Read-only property that resolves to a Literal + * @ReadOnly({ + * through: "post://author", + * resolveLanguage: "literal" + * }) + * author: string = ""; + * + * // Read-only property for system-managed data + * @ReadOnly({ + * through: "post://version", + * initial: "1.0.0" + * }) + * version: string = ""; + * } + * ``` + * + * @param {PropertyOptions} opts - Property configuration + * @param {string} opts.through - The predicate URI for the property + * @param {string} [opts.initial] - Initial value (if property should have one) + * @param {string} [opts.resolveLanguage] - Language to use for value resolution (e.g. "literal") + * @param {string} [opts.getter] - Custom Prolog code for getting the property value + * @param {boolean} [opts.local] - Whether the property should only be stored locally + */ +export function ReadOnly(opts: PropertyOptions) { + return Optional({ + ...opts, + writable: false + }); } \ No newline at end of file From ec27c590cc5395e7dea1900a80042792ea1a799b Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 15:19:41 +0100 Subject: [PATCH 12/15] Ad4mModel doc-comments upgrade --- core/src/subject/Ad4mModel.ts | 124 ++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 19 deletions(-) diff --git a/core/src/subject/Ad4mModel.ts b/core/src/subject/Ad4mModel.ts index d41387636..383b38754 100644 --- a/core/src/subject/Ad4mModel.ts +++ b/core/src/subject/Ad4mModel.ts @@ -172,20 +172,102 @@ function buildLimitQuery(limit?: number): string { } /** - * Class representing a subject entity. - * Can extend this class to create a new subject entity to add methods interact with SDNA and much better experience then using the bare bone methods. + * Base class for defining data models in AD4M. + * + * @description + * Ad4mModel provides the foundation for creating data models that are stored in AD4M perspectives. + * Each model instance is represented as a subgraph in the perspective, with properties and collections + * mapped to links in that graph. The class uses Prolog-based queries to efficiently search and filter + * instances based on their properties and relationships. + * + * Key concepts: + * - Each model instance has a unique base expression that serves as its identifier + * - Properties are stored as links with predicates defined by the `through` option + * - Collections represent one-to-many relationships as sets of links + * - Queries are translated to Prolog for efficient graph pattern matching + * - Changes are tracked through the perspective's subscription system * * @example * ```typescript - * @SDNAClass({ name: "Recipe" }) + * // Define a recipe model + * @ModelOptions({ name: "Recipe" }) * class Recipe extends Ad4mModel { - * @SubjectProperty({ + * // Required property with literal value + * @Property({ * through: "recipe://name", - * writable: true, * resolveLanguage: "literal" * }) * name: string = ""; + * + * // Optional property with custom initial value + * @Optional({ + * through: "recipe://status", + * initial: "recipe://draft" + * }) + * status: string = ""; + * + * // Read-only computed property + * @ReadOnly({ + * through: "recipe://rating", + * getter: ` + * findall(Rating, triple(Base, "recipe://user_rating", Rating), Ratings), + * sum_list(Ratings, Sum), + * length(Ratings, Count), + * Value is Sum / Count + * ` + * }) + * averageRating: number = 0; + * + * // Collection of ingredients + * @Collection({ through: "recipe://ingredient" }) + * ingredients: string[] = []; + * + * // Collection of comments that are instances of another model + * @Collection({ + * through: "recipe://comment", + * where: { isInstance: Comment } + * }) + * comments: Comment[] = []; * } + * + * // Create and save a new recipe + * const recipe = new Recipe(perspective); + * recipe.name = "Chocolate Cake"; + * recipe.ingredients = ["flour", "sugar", "cocoa"]; + * await recipe.save(); + * + * // Query recipes in different ways + * // Get all recipes + * const allRecipes = await Recipe.findAll(perspective); + * + * // Find recipes with specific criteria + * const desserts = await Recipe.findAll(perspective, { + * where: { + * status: "recipe://published", + * averageRating: { gt: 4 } + * }, + * order: { name: "ASC" }, + * limit: 10 + * }); + * + * // Use the fluent query builder + * const popularRecipes = await Recipe.query(perspective) + * .where({ averageRating: { gt: 4.5 } }) + * .order({ averageRating: "DESC" }) + * .limit(5) + * .get(); + * + * // Subscribe to real-time updates + * await Recipe.query(perspective) + * .where({ status: "recipe://cooking" }) + * .subscribe(recipes => { + * console.log("Currently being cooked:", recipes); + * }); + * + * // Paginate results + * const { results, totalCount, pageNumber } = await Recipe.query(perspective) + * .where({ status: "recipe://published" }) + * .paginate(10, 1); * ``` */ export class Ad4mModel { @@ -216,19 +298,22 @@ export class Ad4mModel { } /** - * Constructs a new subject entity instance. + * Constructs a new model instance. * - * @param perspective - The perspective that the subject belongs to - * @param baseExpression - Optional base expression for the subject + * @param perspective - The perspective where this model will be stored + * @param baseExpression - Optional unique identifier for this instance * @param source - Optional source expression this instance is linked to * * @example * ```typescript + * // Create a new recipe with auto-generated base expression * const recipe = new Recipe(perspective); - * // or with base expression - * const recipe = new Recipe(perspective, "base123"); - * // or with source - * const recipe = new Recipe(perspective, "base123", "source456"); + * + * // Create with specific base expression + * const recipe = new Recipe(perspective, "recipe://chocolate-cake"); + * + * // Create with source link + * const recipe = new Recipe(perspective, undefined, "cookbook://desserts"); * ``` */ constructor(perspective: PerspectiveProxy, baseExpression?: string, source?: string) { @@ -574,10 +659,10 @@ export class Ad4mModel { } /** - * Saves the subject entity to the perspective. - * Creates a new subject with the base expression and links it to the source. + * Saves the model instance to the perspective. + * Creates a new instance with the base expression and links it to the source. * - * @throws Will throw if subject creation, linking, or updating fails + * @throws Will throw if instance creation, linking, or updating fails * * @example * ```typescript @@ -598,7 +683,7 @@ export class Ad4mModel { } /** - * Updates the subject entity's properties and collections. + * Updates the model instance's properties and collections. * * @throws Will throw if property setting or collection updates fail * @@ -606,6 +691,7 @@ export class Ad4mModel { * ```typescript * const recipe = await Recipe.findAll(perspective)[0]; * recipe.rating = 5; + * recipe.ingredients.push("garlic"); * await recipe.update(); * ``` */ @@ -641,9 +727,9 @@ export class Ad4mModel { } /** - * Gets the subject entity with all properties and collections populated. + * Gets the model instance with all properties and collections populated. * - * @returns The populated subject entity + * @returns The populated model instance * @throws Will throw if data retrieval fails * * @example @@ -660,7 +746,7 @@ export class Ad4mModel { } /** - * Deletes the subject entity from the perspective. + * Deletes the model instance from the perspective. * * @throws Will throw if removal fails * From 92b16cdd3180010c9ba99a4142953d5aaf804f3e Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 15:23:07 +0100 Subject: [PATCH 13/15] Rename SubjectQueryBuilder to ModelQueryBuilder --- core/src/subject/Ad4mModel.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/subject/Ad4mModel.ts b/core/src/subject/Ad4mModel.ts index 383b38754..5b198db4d 100644 --- a/core/src/subject/Ad4mModel.ts +++ b/core/src/subject/Ad4mModel.ts @@ -787,8 +787,8 @@ export class Ad4mModel { this: typeof Ad4mModel & (new (...args: any[]) => T), perspective: PerspectiveProxy, query?: Query - ): SubjectQueryBuilder { - return new SubjectQueryBuilder(perspective, this as any, query); + ): ModelQueryBuilder { + return new ModelQueryBuilder(perspective, this as any, query); } } @@ -812,7 +812,7 @@ export class Ad4mModel { * }); * ``` */ -export class SubjectQueryBuilder { +export class ModelQueryBuilder { private perspective: PerspectiveProxy; private queryParams: Query = {}; private ctor: typeof Ad4mModel; @@ -839,7 +839,7 @@ export class SubjectQueryBuilder { * }) * ``` */ - where(conditions: Where): SubjectQueryBuilder { + where(conditions: Where): ModelQueryBuilder { this.queryParams.where = conditions; return this; } @@ -855,7 +855,7 @@ export class SubjectQueryBuilder { * .order({ createdAt: "DESC" }) * ``` */ - order(orderBy: Order): SubjectQueryBuilder { + order(orderBy: Order): ModelQueryBuilder { this.queryParams.order = orderBy; return this; } @@ -871,7 +871,7 @@ export class SubjectQueryBuilder { * .limit(10) * ``` */ - limit(limit: number): SubjectQueryBuilder { + limit(limit: number): ModelQueryBuilder { this.queryParams.limit = limit; return this; } @@ -887,7 +887,7 @@ export class SubjectQueryBuilder { * .offset(20) // Skip first 20 results * ``` */ - offset(offset: number): SubjectQueryBuilder { + offset(offset: number): ModelQueryBuilder { this.queryParams.offset = offset; return this; } @@ -903,7 +903,7 @@ export class SubjectQueryBuilder { * .source("ad4m://self") * ``` */ - source(source: string): SubjectQueryBuilder { + source(source: string): ModelQueryBuilder { this.queryParams.source = source; return this; } @@ -919,7 +919,7 @@ export class SubjectQueryBuilder { * .properties(["name", "description", "rating"]) * ``` */ - properties(properties: string[]): SubjectQueryBuilder { + properties(properties: string[]): ModelQueryBuilder { this.queryParams.properties = properties; return this; } @@ -935,7 +935,7 @@ export class SubjectQueryBuilder { * .collections(["ingredients", "steps"]) * ``` */ - collections(collections: string[]): SubjectQueryBuilder { + collections(collections: string[]): ModelQueryBuilder { this.queryParams.collections = collections; return this; } From cc1212ed566eb079d074846f8f322576da01a4d4 Mon Sep 17 00:00:00 2001 From: Nicolas Luck Date: Fri, 21 Mar 2025 15:26:20 +0100 Subject: [PATCH 14/15] Rename subject directory to model --- core/src/index.ts | 6 +++--- core/src/{subject => model}/Ad4mModel.ts | 0 core/src/{subject => model}/Subject.ts | 0 core/src/{subject => model}/decorators.ts | 0 core/src/{subject => model}/util.ts | 0 core/src/perspectives/PerspectiveClient.ts | 2 +- core/src/perspectives/PerspectiveProxy.ts | 6 +++--- 7 files changed, 7 insertions(+), 7 deletions(-) rename core/src/{subject => model}/Ad4mModel.ts (100%) rename core/src/{subject => model}/Subject.ts (100%) rename core/src/{subject => model}/decorators.ts (100%) rename core/src/{subject => model}/util.ts (100%) diff --git a/core/src/index.ts b/core/src/index.ts index 99006524d..6546e12bc 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -19,8 +19,8 @@ export * from "./perspectives/PerspectiveProxy"; export * from "./perspectives/PerspectiveDiff"; export * from "./perspectives/LinkQuery"; export * from "./SmartLiteral"; -export * from "./subject/decorators"; -export * from "./subject/Subject"; +export * from "./model/decorators"; +export * from "./model/Subject"; export * from "./neighbourhood/Neighbourhood"; export * from "./neighbourhood/NeighbourhoodProxy"; export * from "./typeDefs"; @@ -30,4 +30,4 @@ export * from "./agent/AgentClient"; export * from "./ai/AIClient" export * from "./ai/Tasks" export * from "./runtime/RuntimeResolver" -export * from './subject/Ad4mModel' +export * from './model/Ad4mModel' diff --git a/core/src/subject/Ad4mModel.ts b/core/src/model/Ad4mModel.ts similarity index 100% rename from core/src/subject/Ad4mModel.ts rename to core/src/model/Ad4mModel.ts diff --git a/core/src/subject/Subject.ts b/core/src/model/Subject.ts similarity index 100% rename from core/src/subject/Subject.ts rename to core/src/model/Subject.ts diff --git a/core/src/subject/decorators.ts b/core/src/model/decorators.ts similarity index 100% rename from core/src/subject/decorators.ts rename to core/src/model/decorators.ts diff --git a/core/src/subject/util.ts b/core/src/model/util.ts similarity index 100% rename from core/src/subject/util.ts rename to core/src/model/util.ts diff --git a/core/src/perspectives/PerspectiveClient.ts b/core/src/perspectives/PerspectiveClient.ts index 87a6f94d0..514380035 100644 --- a/core/src/perspectives/PerspectiveClient.ts +++ b/core/src/perspectives/PerspectiveClient.ts @@ -10,7 +10,7 @@ import { Perspective } from "./Perspective"; import { PerspectiveHandle, PerspectiveState } from "./PerspectiveHandle"; import { LinkStatus, PerspectiveProxy } from './PerspectiveProxy'; import { AIClient } from "../ai/AIClient"; -import { AllInstancesResult } from "../subject/Ad4mModel"; +import { AllInstancesResult } from "../model/Ad4mModel"; const LINK_EXPRESSION_FIELDS = ` author diff --git a/core/src/perspectives/PerspectiveProxy.ts b/core/src/perspectives/PerspectiveProxy.ts index 687369af2..11c875125 100644 --- a/core/src/perspectives/PerspectiveProxy.ts +++ b/core/src/perspectives/PerspectiveProxy.ts @@ -4,15 +4,15 @@ import { LinkQuery } from "./LinkQuery"; import { PerspectiveHandle, PerspectiveState } from './PerspectiveHandle' import { Perspective } from "./Perspective"; import { Literal } from "../Literal"; -import { Subject } from "../subject/Subject"; +import { Subject } from "../model/Subject"; import { ExpressionRendered } from "../expression/Expression"; -import { collectionAdderToName, collectionRemoverToName, collectionSetterToName } from "../subject/util"; +import { collectionAdderToName, collectionRemoverToName, collectionSetterToName } from "../model/util"; import { NeighbourhoodProxy } from "../neighbourhood/NeighbourhoodProxy"; import { NeighbourhoodExpression } from "../neighbourhood/Neighbourhood"; import { AIClient } from "../ai/AIClient"; import { PERSPECTIVE_QUERY_SUBSCRIPTION } from "./PerspectiveResolver"; import { gql } from "@apollo/client/core"; -import { AllInstancesResult } from "../subject/Ad4mModel"; +import { AllInstancesResult } from "../model/Ad4mModel"; type QueryCallback = (result: AllInstancesResult) => void; From 4ee8610e78651cc2e7b821cec8f66b0baee1ff6c Mon Sep 17 00:00:00 2001 From: jhweir Date: Fri, 21 Mar 2025 15:04:20 +0000 Subject: [PATCH 15/15] Small comment updates --- core/src/model/Ad4mModel.ts | 6 ++--- tests/js/tests/prolog-and-literals.test.ts | 27 +++++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/core/src/model/Ad4mModel.ts b/core/src/model/Ad4mModel.ts index 5b198db4d..5e7d59b78 100644 --- a/core/src/model/Ad4mModel.ts +++ b/core/src/model/Ad4mModel.ts @@ -476,7 +476,7 @@ export class Ad4mModel { } /** - * Gets all instances with count of total matches without limit applied. + * Gets all instances with count of total matches without offset & limit applied. * * @param perspective - The perspective to search in * @param query - Optional query parameters to filter results @@ -778,7 +778,7 @@ export class Ad4mModel { * // With real-time updates * await Recipe.query(perspective) * .where({ status: "cooking" }) - * .subscribeAndRun(recipes => { + * .subscribe(recipes => { * console.log("Currently cooking:", recipes); * }); * ``` @@ -807,7 +807,7 @@ export class Ad4mModel { * const recipes = await builder.run(); * * // Or subscribe to updates - * await builder.subscribeAndRun(recipes => { + * await builder.subscribe(recipes => { * console.log("Updated recipes:", recipes); * }); * ``` diff --git a/tests/js/tests/prolog-and-literals.test.ts b/tests/js/tests/prolog-and-literals.test.ts index 8a21a9e4b..45c5a036c 100644 --- a/tests/js/tests/prolog-and-literals.test.ts +++ b/tests/js/tests/prolog-and-literals.test.ts @@ -1520,7 +1520,7 @@ describe("Prolog + Literals", () => { await recipe3.delete(); }) - it("findAllAndCount() returns both the retrived instances and a count without limit applied", async () => { + it("findAllAndCount() returns both the retrived instances and the total count", async () => { // Clear any old recipes const oldRecipes = await Recipe.findAll(perspective!); for (const recipe of oldRecipes) await recipe.delete(); @@ -1547,33 +1547,32 @@ describe("Prolog + Literals", () => { expect(recipes3.length).to.equal(3); expect(count3).to.equal(6); - // Test count with limit & where constraints + // Test count with where constraints & limit const { results: recipes2, totalCount: count2 } = await Recipe.findAllAndCount(perspective!, { where: { name: ["Recipe 1", "Recipe 2", "Recipe 3"] }, limit: 2, count: true }); expect(recipes2.length).to.equal(2); expect(count2).to.equal(3); - // Test count with offset, limit, & where equality constraint (exists) - const { results: recipes4, totalCount: count4 } = await Recipe.findAllAndCount(perspective!, { where: { number: 3 }, offset: 3, limit: 3, count: true }); - //console.log('recipes4: ', recipes4); - expect(recipes4.length).to.equal(0); - expect(count4).to.equal(0); + // Test count with where equality constraint (exists), offset, & limit + const { results: recipes4, totalCount: count4 } = await Recipe.findAllAndCount(perspective!, { where: { number: 5 }, offset: 3, limit: 3, count: true }); + expect(recipes4.length).to.equal(3); + expect(count4).to.equal(6); - // Test count with offset, limit, & where equality constraint (does not exist) - const { results: recipes5, totalCount: count5 } = await Recipe.findAllAndCount(perspective!, { where: { number: 5 }, offset: 3, limit: 3, count: true }); - expect(recipes5.length).to.equal(3); - expect(count5).to.equal(6); + // Test count with where equality constraint (does not exist), offset, & limit + const { results: recipes5, totalCount: count5 } = await Recipe.findAllAndCount(perspective!, { where: { number: 3 }, offset: 3, limit: 3, count: true }); + expect(recipes5.length).to.equal(0); + expect(count5).to.equal(0); - // Test count with limit & where not constraint + // Test count with where not constraint & limit const { results: recipes6, totalCount: count6 } = await Recipe.findAllAndCount(perspective!, { where: { name: { not: "Recipe 1" } }, limit: 3, count: true }); expect(recipes6.length).to.equal(3); expect(count6).to.equal(5); - // Test count with offset, limit, & where not constraint + // Test count with where not constraint, offset, & limit const { results: recipes7, totalCount: count7 } = await Recipe.findAllAndCount(perspective!, { where: { name: { not: "Recipe 2" } }, offset: 1, limit: 3, count: true }); expect(recipes7.length).to.equal(3); expect(count7).to.equal(5); - // Test count with offset, limit, & where constraint + // Test count with where not constraint, offset, & limit greater than remaining results const { results: recipes8, totalCount: count8 } = await Recipe.findAllAndCount(perspective!, { where: { name: { not: "Recipe 4" } }, offset: 3, limit: 3, count: true }); expect(recipes8.length).to.equal(2); expect(count8).to.equal(5);