Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d9b727a
feat: add isInstance filtering for Ad4mModel collections
jhweir Jan 27, 2026
1256af3
feat(core): add surrealGetter support for custom graph traversals in …
jhweir Jan 28, 2026
8d6403c
ad4m/core version bump to 0.11.2-dev.3
jhweir Jan 28, 2026
e2c510b
docs: Document SDNA parallel calls limitation and solutions
jhweir Jan 28, 2026
1905ff1
feat: Track both createdAt and updatedAt timestamps in Ad4mModel
jhweir Jan 28, 2026
62a0fd5
Bump ad4m core to version 0.11.2-dev.4
jhweir Jan 28, 2026
3c14940
docs: Add comprehensive AD4M model system analysis and redesign propo…
jhweir Jan 30, 2026
0b5c094
fix(ad4m): resolve timestamp property assignment errors and improve e…
jhweir Jan 30, 2026
c1026de
z-index increase on ad4m-connect modal
jhweir Feb 3, 2026
66f0bec
fix(connect): improve z-index handling and add mobile detection
jhweir Feb 3, 2026
158a6cd
fix(connect): handle invalid token and auth state change events
jhweir Feb 5, 2026
a5e6f41
ad4m-connect version bumped to 0.11.2-dev.5
jhweir Feb 5, 2026
1389a97
fix(Ad4mModel): Fix property filtering, collection instance checks, a…
jhweir Feb 5, 2026
f375fea
ad4m/core bumped to version 0.11.2-dev.5
jhweir Feb 6, 2026
39b98c2
Settings button size decreased
jhweir Feb 10, 2026
607e8fa
Strategy docs removed
jhweir Feb 11, 2026
f62ec21
feat: Add comprehensive tests for surrealGetter and isInstance featur…
jhweir Feb 11, 2026
f5327bb
feat(core): add surrealCondition filtering for collections
jhweir Feb 12, 2026
3a01504
Merge branch 'dev' into feat/collection-isinstance-filter
lucksus Feb 12, 2026
87b61cc
refactor(core): standardize query engine naming - SurrealDB as default
jhweir Feb 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,5 @@
"@coasys/ad4m": "*"
}
},
"version": "0.11.2-dev.4"
"version": "0.11.2-dev.5"
}
110 changes: 62 additions & 48 deletions connect/src/components/views/ConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class ConnectionOptions extends LitElement {
@state() private localNodeDetected = false;
@state() private newPort = 0;
@state() private newRemoteUrl = "";
@state() private isMobile = false;

static styles = [
sharedStyles,
Expand Down Expand Up @@ -83,14 +84,25 @@ export class ConnectionOptions extends LitElement {
this.detectLocalNode();
}

private checkMobile = () => {
this.isMobile = window.innerWidth < 800;
};

async connectedCallback() {
super.connectedCallback();
this.newPort = this.port;
this.newRemoteUrl = this.remoteUrl || "";
this.checkMobile();
window.addEventListener('resize', this.checkMobile);
await this.detectLocalNode();
this.loading = false;
}

disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('resize', this.checkMobile);
}

willUpdate(changedProps: PropertyValues) {
if (changedProps.has('port')) this.newPort = this.port;
if (changedProps.has('remoteUrl')) this.newRemoteUrl = this.remoteUrl || "";
Expand All @@ -107,56 +119,58 @@ export class ConnectionOptions extends LitElement {
</div>

<div class="options">
<div class="box">
<div class="box-header">
${LocalIcon()}
<h3>Local Node</h3>
</div>
${!this.isMobile ? html`
<div class="box">
<div class="box-header">
${LocalIcon()}
<h3>Local Node</h3>
</div>

${this.localNodeDetected
? html`
<div class="state success">
${CheckIcon()}
<p>Local node detected on port ${this.port}</p>
</div>

<button class="primary" @click=${this.connectLocalNode}>
Connect to Local Node
</button>
`
: html`
<div class="state danger">
${CrossIcon()}
<p>No local node detected on port ${this.port}</p>
</div>

<p style="margin-bottom: -12px">Download and install AD4M</p>
<button class="secondary" @click=${() => window.open("https://github.com/coasys/ad4m/releases")}>
${DownloadIcon()} Download AD4M
</button>
`
}

<p style="margin-bottom: -12px">Or try another port</p>
<div class="port-input">
<input
type="number"
placeholder="Port number..."
.value=${this.newPort != null ? this.newPort.toString() : ''}
@input=${(e: Event) => {
const input = e.target as HTMLInputElement;
const next = Number.parseInt(input.value, 10);
if (Number.isFinite(next)) this.newPort = next;
}}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter') this.refreshPort();
}}
/>
<button class="primary" @click=${this.refreshPort}>
${RefreshIcon()}
</button>
${this.localNodeDetected
? html`
<div class="state success">
${CheckIcon()}
<p>Local node detected on port ${this.port}</p>
</div>

<button class="primary" @click=${this.connectLocalNode}>
Connect to Local Node
</button>
`
: html`
<div class="state danger">
${CrossIcon()}
<p>No local node detected on port ${this.port}</p>
</div>

<p style="margin-bottom: -12px">Download and install AD4M</p>
<button class="secondary" @click=${() => window.open("https://github.com/coasys/ad4m/releases")}>
${DownloadIcon()} Download AD4M
</button>
`
}

<p style="margin-bottom: -12px">Or try another port</p>
<div class="port-input">
<input
type="number"
placeholder="Port number..."
.value=${this.newPort != null ? this.newPort.toString() : ''}
@input=${(e: Event) => {
const input = e.target as HTMLInputElement;
const next = Number.parseInt(input.value, 10);
if (Number.isFinite(next)) this.newPort = next;
}}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter') this.refreshPort();
}}
/>
<button class="primary" @click=${this.refreshPort}>
${RefreshIcon()}
</button>
</div>
</div>
</div>
` : ''}

${this.showMultiUserOption ? html`
<div class="box">
Expand Down
37 changes: 23 additions & 14 deletions connect/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ export default class Ad4mConnect extends EventTarget {
console.log('[Ad4m Connect] Embedded mode - waiting for AD4M config via postMessage');

return new Promise((resolve, reject) => {
// Set up timeout
// Set up 30 second timeout
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for AD4M config from parent window'));
}, 30000); // 30 second timeout
}, 30000);

// Store resolvers to call when AD4M_CONFIG arrives
this.embeddedResolve = (client: Ad4mClient) => {
Expand All @@ -65,14 +65,16 @@ export default class Ad4mConnect extends EventTarget {
};

// If we already have a client (message arrived before connect() was called)
if (this.ad4mClient && this.authState === 'authenticated') {
clearTimeout(timeout);
console.log('[Ad4m Connect] Client already initialized in embedded mode');
resolve(this.ad4mClient);
} else if (this.ad4mClient && this.authState !== 'authenticated') {
// Auth already failed before connect() was called
clearTimeout(timeout);
reject(new Error(`Embedded auth state: ${this.authState}`));
if (this.ad4mClient) {
if (this.authState === 'authenticated') {
clearTimeout(timeout);
console.log('[Ad4m Connect] Client already initialized in embedded mode');
resolve(this.ad4mClient);
} else {
// Auth already failed before connect() was called
clearTimeout(timeout);
reject(new Error(`Embedded auth state: ${this.authState}`));
}
}
});
}
Expand Down Expand Up @@ -186,14 +188,22 @@ export default class Ad4mConnect extends EventTarget {
} catch (error) {
console.error('[Ad4m Connect] Authentication check failed:', error);
const lockedMessage = "Cannot extractByTags from a ciphered wallet. You must unlock first.";

if (error.message === lockedMessage) {
// TODO: isLocked throws an error, should just return a boolean. Temp fix
this.notifyAuthChange("locked");
return true;
} else {
this.notifyAuthChange("unauthenticated");
return false;
}

// Clear token if it's invalid (signed by different agent)
if (error.message === "InvalidSignature") {
console.log('[Ad4m Connect] Clearing invalid token due to InvalidSignature');
this.token = '';
removeLocal('ad4m-token');
}

this.notifyAuthChange("unauthenticated");
return false;
}
}

Expand Down Expand Up @@ -455,7 +465,6 @@ export default class Ad4mConnect extends EventTarget {
}

private notifyAuthChange(value: AuthStates) {
if (this.authState === value) return;
this.authState = value;
this.dispatchEvent(new CustomEvent("authstatechange", { detail: value }));

Expand Down
13 changes: 7 additions & 6 deletions connect/src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const styles = css`
left: 0;
height: 100vh;
width: 100vw;
z-index: 100;
z-index: 99999;
}

.backdrop {
Expand Down Expand Up @@ -90,12 +90,13 @@ const styles = css`
background: transparent;
padding: 0;
cursor: pointer;
position: absolute;
bottom: 20px;
right: 20px;
position: fixed;
bottom: 10px;
right: 10px;
color: var(--ac-primary-color);
width: 40px;
height: 40px;
width: 34px;
height: 34px;
z-index: 99999;
}
`;

Expand Down
2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@
"graphql@15.7.2": "patches/graphql@15.7.2.patch"
}
},
"version": "0.11.2-dev.2"
"version": "0.11.2-dev.5"
}
20 changes: 10 additions & 10 deletions core/src/model/Ad4mModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("Ad4mModel.getModelMetadata()", () => {
@Optional({ through: "test://optional", writable: true })
optional: string = "";

@ReadOnly({ through: "test://readonly", getter: "custom_getter" })
@ReadOnly({ through: "test://readonly", prologGetter: "custom_getter" })
readonly: string = "";

@Flag({ through: "test://type", value: "test://flag" })
Expand All @@ -47,7 +47,7 @@ describe("Ad4mModel.getModelMetadata()", () => {
// Verify "readonly" property
expect(metadata.properties.readonly.predicate).toBe("test://readonly");
expect(metadata.properties.readonly.writable).toBe(false);
expect(metadata.properties.readonly.getter).toBe("custom_getter");
expect(metadata.properties.readonly.prologGetter).toBe("custom_getter");

// Verify "type" property (flag)
expect(metadata.properties.type.predicate).toBe("test://type");
Expand Down Expand Up @@ -114,18 +114,18 @@ describe("Ad4mModel.getModelMetadata()", () => {
class CustomModel extends Ad4mModel {
@Optional({
through: "test://computed",
getter: "triple(Base, 'test://value', V), Value is V * 2",
setter: "Value is V / 2, Actions = [{action: 'setSingleTarget', source: 'this', predicate: 'test://value', target: Value}]"
prologGetter: "triple(Base, 'test://value', V), Value is V * 2",
prologSetter: "Value is V / 2, Actions = [{action: 'setSingleTarget', source: 'this', predicate: 'test://value', target: Value}]"
})
computed: number = 0;
}

const metadata = CustomModel.getModelMetadata();

// Assert getter and setter contain the custom code
expect(metadata.properties.computed.getter).toContain("triple(Base, 'test://value', V), Value is V * 2");
expect(metadata.properties.computed.setter).toContain("Value is V / 2");
expect(metadata.properties.computed.setter).toContain("setSingleTarget");
// Assert prologGetter and prologSetter contain the custom code
expect(metadata.properties.computed.prologGetter).toContain("triple(Base, 'test://value', V), Value is V * 2");
expect(metadata.properties.computed.prologSetter).toContain("Value is V / 2");
expect(metadata.properties.computed.prologSetter).toContain("setSingleTarget");
});

it("should handle collection with isInstance where clause", () => {
Expand Down Expand Up @@ -163,7 +163,7 @@ describe("Ad4mModel.getModelMetadata()", () => {
@Optional({ through: "recipe://description" })
description: string = "";

@ReadOnly({ through: "recipe://rating", getter: "avg_rating(Base, Value)" })
@ReadOnly({ through: "recipe://rating", prologGetter: "avg_rating(Base, Value)" })
rating: number = 0;

@Collection({ through: "recipe://ingredient" })
Expand Down Expand Up @@ -194,7 +194,7 @@ describe("Ad4mModel.getModelMetadata()", () => {
expect(metadata.properties.name.resolveLanguage).toBe("literal");
expect(metadata.properties.description.predicate).toBe("recipe://description");
expect(metadata.properties.rating.predicate).toBe("recipe://rating");
expect(metadata.properties.rating.getter).toBe("avg_rating(Base, Value)");
expect(metadata.properties.rating.prologGetter).toBe("avg_rating(Base, Value)");
expect(metadata.collections.ingredients.predicate).toBe("recipe://ingredient");
expect(metadata.collections.steps.predicate).toBe("recipe://step");
expect(metadata.collections.steps.local).toBe(true);
Expand Down
Loading