Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **ObjectQL build failure** — Fixed TypeScript TS2345 errors in `packages/objectql/src/protocol.ts`
where `SchemaRegistry.registerItem()` calls failed type checking for the `keyField` parameter.
Applied `'name' as any` cast consistent with the established codebase pattern.
- **ObjectQL `loadMetaFromDb`** — Fixed metadata hydration for `object` type records to use
`SchemaRegistry.registerObject()` instead of `registerItem()`, resolving a mismatch where
objects registered via `registerItem` could not be retrieved via `getItem('object', ...)`.
- **Adapter discovery endpoints** — Fixed discovery route in Hono, SvelteKit, Nuxt, Next.js,
and Fastify adapters to serve discovery info at the API prefix root (e.g., `GET /api`)
instead of a `/discovery` subpath. Updated `.well-known/objectstack` redirects accordingly.
- **Client feed namespace routing** — Fixed `ObjectStackClient.feed` methods to use the `data`
route (`/api/v1/data/{object}/{recordId}/feed`) instead of a separate `/api/v1/feed` route,
matching the actual server-side routing where feed is a sub-resource of data.

### Added
- **`@objectstack/service-ai` — Unified AI capability service plugin** — New kernel plugin
providing standardized AI service integration:
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/fastify/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -76,13 +76,13 @@ export async function objectStackPlugin(fastify: FastifyInstance, options: Fasti
// ─── Explicit routes (framework-specific handling required) ────────────────

// --- Discovery ---
fastify.get(`${prefix}/discovery`, async (_request: FastifyRequest, reply: FastifyReply) => {
fastify.get(prefix, async (_request: FastifyRequest, reply: FastifyReply) => {
return reply.send({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

// --- .well-known ---
fastify.get('/.well-known/objectstack', async (_request: FastifyRequest, reply: FastifyReply) => {
return reply.redirect(`${prefix}/discovery`);
return reply.redirect(prefix);
});

// --- Auth (needs auth service integration) ---
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/hono/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,13 +81,13 @@ export function createHonoApp(options: ObjectStackHonoOptions): Hono {
// ─── Explicit routes (framework-specific handling required) ────────────────

// --- Discovery ---
app.get(`${prefix}/discovery`, async (c) => {
app.get(prefix, async (c) => {
return c.json({ data: await dispatcher.getDiscoveryInfo(prefix) });
});

// --- .well-known ---
app.get('/.well-known/objectstack', (c) => {
return c.redirect(`${prefix}/discovery`);
return c.redirect(prefix);
});

// --- Auth (needs auth service integration) ---
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/nextjs/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ export function createRouteHandler(options: NextAdapterOptions) {
const method = req.method;

// --- 0. Discovery Endpoint ---
if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
if (segments.length === 0 && method === 'GET') {
return NextResponse.json({ data: await dispatcher.getDiscoveryInfo(options.prefix || '/api') });
}

Expand DownExpand Up@@ -152,7 +152,7 @@ export function createDiscoveryHandler(options: NextAdapterOptions) {
return async function discoveryHandler(req: NextRequest) {
const apiPath = options.prefix || '/api';
const url = new URL(req.url);
const targetUrl = new URL(`${apiPath}/discovery`, url.origin);
const targetUrl = new URL(apiPath, url.origin);
return NextResponse.redirect(targetUrl);
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/adapters/nuxt/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,7 +89,7 @@ export function createH3Router(options: NuxtAdapterOptions): Router {

// --- Discovery ---
router.get(
`${prefix}/discovery`,
prefix,
defineEventHandler(async () => {
return { data: await dispatcher.getDiscoveryInfo(prefix) };
}),
Expand All@@ -99,7 +99,7 @@ export function createH3Router(options: NuxtAdapterOptions): Router {
router.get(
'/.well-known/objectstack',
defineEventHandler((event) => {
return sendRedirect(event, `${prefix}/discovery`);
return sendRedirect(event, prefix);
}),
);

Expand Down
2 changes: 1 addition & 1 deletion packages/adapters/sveltekit/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,7 @@ export function createRequestHandler(options: SvelteKitAdapterOptions) {
const segments = path.split('/').filter(Boolean);

// --- Discovery ---
if (segments.length === 1 && segments[0] === 'discovery' && method === 'GET') {
if (segments.length === 0 && method === 'GET') {
return new Response(JSON.stringify({ data: await dispatcher.getDiscoveryInfo(prefix) }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
Expand Down
28 changes: 14 additions & 14 deletions packages/client/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1294,7 +1294,7 @@ export class ObjectStackClient {
* List feed items for a record
*/
list: async (object: string, recordId: string, options?: { type?: string; limit?: number; cursor?: string }): Promise<GetFeedResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const params = new URLSearchParams();
if (options?.type) params.set('type', options.type);
if (options?.limit) params.set('limit', String(options.limit));
Expand All@@ -1308,7 +1308,7 @@ export class ObjectStackClient {
* Create a new feed item (comment, note, task, etc.)
*/
create: async (object: string, recordId: string, data: { type: string; body?: string; mentions?: any[]; parentId?: string; visibility?: string }): Promise<CreateFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed`, {
method: 'POST',
body: JSON.stringify(data)
Expand All@@ -1320,7 +1320,7 @@ export class ObjectStackClient {
* Update an existing feed item
*/
update: async (object: string, recordId: string, feedId: string, data: { body?: string; mentions?: any[]; visibility?: string }): Promise<UpdateFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
method: 'PUT',
body: JSON.stringify(data)
Expand All@@ -1332,7 +1332,7 @@ export class ObjectStackClient {
* Delete a feed item
*/
delete: async (object: string, recordId: string, feedId: string): Promise<DeleteFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}`, {
method: 'DELETE'
});
Expand All@@ -1343,7 +1343,7 @@ export class ObjectStackClient {
* Add an emoji reaction to a feed item
*/
addReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<AddReactionResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions`, {
method: 'POST',
body: JSON.stringify({ emoji })
Expand All@@ -1355,7 +1355,7 @@ export class ObjectStackClient {
* Remove an emoji reaction from a feed item
*/
removeReaction: async (object: string, recordId: string, feedId: string, emoji: string): Promise<RemoveReactionResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/reactions/${encodeURIComponent(emoji)}`, {
method: 'DELETE'
});
Expand All@@ -1366,7 +1366,7 @@ export class ObjectStackClient {
* Pin a feed item to the top of the timeline
*/
pin: async (object: string, recordId: string, feedId: string): Promise<PinFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
method: 'POST'
});
Expand All@@ -1377,7 +1377,7 @@ export class ObjectStackClient {
* Unpin a feed item
*/
unpin: async (object: string, recordId: string, feedId: string): Promise<UnpinFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/pin`, {
method: 'DELETE'
});
Expand All@@ -1388,7 +1388,7 @@ export class ObjectStackClient {
* Star (bookmark) a feed item
*/
star: async (object: string, recordId: string, feedId: string): Promise<StarFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
method: 'POST'
});
Expand All@@ -1399,7 +1399,7 @@ export class ObjectStackClient {
* Unstar a feed item
*/
unstar: async (object: string, recordId: string, feedId: string): Promise<UnstarFeedItemResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/feed/${encodeURIComponent(feedId)}/star`, {
method: 'DELETE'
});
Expand All@@ -1410,7 +1410,7 @@ export class ObjectStackClient {
* Search feed items
*/
search: async (object: string, recordId: string, query: string, options?: { type?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<SearchFeedResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const params = new URLSearchParams();
params.set('query', query);
if (options?.type) params.set('type', options.type);
Expand All@@ -1427,7 +1427,7 @@ export class ObjectStackClient {
* Get field-level changelog for a record
*/
getChangelog: async (object: string, recordId: string, options?: { field?: string; actorId?: string; dateFrom?: string; dateTo?: string; limit?: number; cursor?: string }): Promise<GetChangelogResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const params = new URLSearchParams();
if (options?.field) params.set('field', options.field);
if (options?.actorId) params.set('actorId', options.actorId);
Expand All@@ -1444,7 +1444,7 @@ export class ObjectStackClient {
* Subscribe to record notifications
*/
subscribe: async (object: string, recordId: string, options?: { events?: string[]; channels?: string[] }): Promise<SubscribeResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
method: 'POST',
body: JSON.stringify(options || {})
Expand All@@ -1456,7 +1456,7 @@ export class ObjectStackClient {
* Unsubscribe from record notifications
*/
unsubscribe: async (object: string, recordId: string): Promise<UnsubscribeResponse> => {
const route = this.getRoute('feed');
const route = this.getRoute('data');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(object)}/${encodeURIComponent(recordId)}/subscribe`, {
method: 'DELETE'
});
Expand Down
14 changes: 9 additions & 5 deletions packages/objectql/src/protocol.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -205,7 +205,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
? JSON.parse(record.metadata)
: record.metadata;
// Hydrate back into registry
SchemaRegistry.registerItem(request.type, data, 'name');
SchemaRegistry.registerItem(request.type, data, 'name' as any);
return data;
Comment on lines 205 to 209

CopilotAIMar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DB fallback hydration always uses SchemaRegistry.registerItem(...). For type === 'object', SchemaRegistry.getItem('object', ...) delegates to getObject() (objectContributors), so registering via registerItem won’t make future object lookups/listing work. Consider branching on record.type/request.type === 'object' and hydrating object records via SchemaRegistry.registerObject(...) (and pass namespace/package id from the sys_metadata row so FQNs and package scoping are preserved).

Copilot uses AI. Check for mistakes.
});
} else {
Expand All@@ -219,7 +219,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
const data = typeof record.metadata === 'string'
? JSON.parse(record.metadata)
: record.metadata;
SchemaRegistry.registerItem(request.type, data, 'name');
SchemaRegistry.registerItem(request.type, data, 'name' as any);
return data;
});
}
Expand DownExpand Up@@ -254,7 +254,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
? JSON.parse(record.metadata)
: record.metadata;
// Hydrate back into registry for next time
SchemaRegistry.registerItem(request.type, item, 'name');
SchemaRegistry.registerItem(request.type, item, 'name' as any);
} else {
// Try alternate type name
const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
Expand All@@ -266,7 +266,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
? JSON.parse(altRecord.metadata)
: altRecord.metadata;
// Hydrate back into registry for next time
SchemaRegistry.registerItem(request.type, item, 'name');
SchemaRegistry.registerItem(request.type, item, 'name' as any);
}
}
} catch {
Expand DownExpand Up@@ -980,7 +980,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
const data = typeof record.metadata === 'string'
? JSON.parse(record.metadata)
: record.metadata;
SchemaRegistry.registerItem(record.type, data, 'name');
if (record.type === 'object') {
SchemaRegistry.registerObject(data as any, record.packageId || 'sys_metadata');
} else {
SchemaRegistry.registerItem(record.type, data, 'name' as any);
Comment on lines +983 to +986

CopilotAIMar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadMetaFromDb() is reading record.packageId, but the persisted sys_metadata field is package_id (see SysMetadataObject). As written, this will almost always fall back to 'sys_metadata', losing package ownership. Also, SchemaRegistry.registerObject should likely receive the record’s namespace (or data.namespace) so objects are registered under the correct FQN rather than the short name. Consider using record.package_id ?? record.packageId and passing record.namespace (and for non-object types, pass the package id into registerItem(..., packageId) so listItems(type, packageId) works).

Suggested change
if(record.type==='object'){
SchemaRegistry.registerObject(dataasany,record.packageId||'sys_metadata');
}else{
SchemaRegistry.registerItem(record.type,data,'name'asany);
constpackageId=(recordasany).package_id??record.packageId??'sys_metadata';
constnamespace=(recordasany).namespace??(dataasany)?.namespace;
if(record.type==='object'){
SchemaRegistry.registerObject(dataasany,packageId,namespace);
}else{
SchemaRegistry.registerItem(record.type,data,'name'asany,packageId);

Copilot uses AI. Check for mistakes.
}
loaded++;
} catch (e) {
errors++;
Expand Down
Loading