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
25 changes: 25 additions & 0 deletions .changeset/retire-feed-discovery-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@objectstack/spec": minor
"@objectstack/metadata-protocol": patch
"@objectstack/client": patch
---

**Breaking (discovery response shape): retire the residual feed capability surface (#3180, follow-up to #1959 / ADR-0052 §5).**

The feed backend was retired long ago; #1959 removed the feed contracts + SDK. This
removes the last discovery/dispatcher references to it, and fixes a real bug where the
`comments` capability was permanently `false`.

- `@objectstack/spec` — `WellKnownCapabilitiesSchema.feed` and `ApiRoutesSchema.feed`
(`routes.feed`) are **removed**, and the `/api/v1/feed` entry is dropped from
`DEFAULT_DISPATCHER_ROUTES`. FROM → TO: clients reading `discovery.capabilities.feed`
or `discovery.routes.feed` → use `discovery.capabilities.comments`; comments/activity
are served by the generic data API on `sys_comment` / `sys_activity`
(`/api/v1/data/sys_comment/…`).
- `@objectstack/metadata-protocol` — `getDiscovery()` no longer emits the always-`false`
`feed` service/capability. **Bug fix:** the `comments` capability previously keyed off
the deleted `'feed'` service (so it was permanently `false` after #1955); it now tracks
the presence of the `sys_comment` object (provided by the always-on audit slate), so
`declared === enforced`.
- `@objectstack/client` — the internal `feed: '/api/v1/feed'` route constant is removed
(it only existed to satisfy the now-removed `ApiRoutes.feed` type; no client code used it).
4 changes: 1 addition & 3 deletions content/docs/references/api/discovery.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ const result = ApiRoutes.parse(data);
| **notifications** | `string` | optional | e.g. /api/v1/notifications |
| **ai** | `string` | optional | e.g. /api/v1/ai |
| **i18n** | `string` | optional | e.g. /api/v1/i18n |
| **feed** | `string` | optional | e.g. /api/v1/feed |


---
Expand Down Expand Up @@ -155,8 +154,7 @@ Well-known capability flags for frontend intelligent adaptation

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **feed** | `boolean` | ✅ | Whether the backend supports Feed / Chatter API |
| **comments** | `boolean` | ✅ | Whether the backend supports comments (a subset of Feed) |
| **comments** | `boolean` | ✅ | Whether the backend supports record comments / chatter (the `sys_comment` object served via the data API) |
| **automation** | `boolean` | ✅ | Whether the backend supports Automation CRUD (flows, triggers) |
| **cron** | `boolean` | ✅ | Whether the backend supports cron scheduling |
| **search** | `boolean` | ✅ | Whether the backend supports full-text search |
Expand Down
3 changes: 1 addition & 2 deletions packages/client/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,6 @@ describe('ObjectStackClient.automation', () => {

it('should expose capabilities after connect', async () => {
const caps = {
feed: true,
comments: true,
automation: false,
cron: false,
Expand All @@ -887,7 +886,7 @@ describe('ObjectStackClient.automation', () => {

await client.connect();
expect(client.capabilities).toBeDefined();
expect(client.capabilities!.feed).toBe(true);
expect(client.capabilities!.comments).toBe(true);
expect(client.capabilities!.automation).toBe(false);
expect(client.capabilities!.search).toBe(true);
});
Expand Down
5 changes: 0 additions & 5 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3202,11 +3202,6 @@ export class ObjectStackClient {
notifications: '/api/v1/notifications',
ai: '/api/v1/ai',
i18n: '/api/v1/i18n',
// NOTE: the `feed` route constant is retained only to satisfy the
// discovery `ApiRoutes` type (`routes.feed`), which is a separate
// follow-up cleanup. The feed SDK accessor + backend were retired
// (ADR-0052 §5); no client code consumes this route.
feed: '/api/v1/feed',
graphql: '/graphql',
};

Expand Down
23 changes: 5 additions & 18 deletions packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1307,22 +1307,6 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
}
}

// Add feed service status
if (registeredServices.has('feed')) {
services['feed'] = {
enabled: true,
status: 'available' as const,
route: '/api/v1/data',
provider: 'service-feed',
};
} else {
services['feed'] = {
enabled: false,
status: 'unavailable' as const,
message: 'Install service-feed to enable',
};
}

const routes: ApiRoutes = {
data: '/api/v1/data',
metadata: '/api/v1/meta',
Expand All @@ -1333,8 +1317,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
// DiscoverySchema defines capabilities as Record<string, { enabled, features?, description? }>
// (hierarchical format). We also keep a flat WellKnownCapabilities for backward compat.
const wellKnown: WellKnownCapabilities = {
feed: registeredServices.has('feed'),
comments: registeredServices.has('feed'),
// Comments/chatter are served by the `sys_comment` object via the generic
// data API (ADR-0052 §5) — not a dedicated service. The capability is true
// iff that object is loaded (the always-on audit slate provides it); this
// keeps declared === enforced (Prime Directive #10). #3180
comments: !!this.engine.registry?.getObject?.('sys_comment'),
automation: registeredServices.has('automation'),
cron: registeredServices.has('job'),
search: registeredServices.has('search'),
Expand Down
22 changes: 17 additions & 5 deletions packages/objectql/src/protocol-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
// workflow is registered but doesn't map to a well-known capability directly
expect(discovery.services.workflow.enabled).toBe(true);
// All well-known capabilities should be disabled since workflow doesn't map to any
expect(discovery.capabilities!.feed).toEqual({ enabled: false });
// (comments derives from the sys_comment object, which is not registered here).
expect(discovery.capabilities!.comments).toEqual({ enabled: false });
expect(discovery.capabilities!.automation).toEqual({ enabled: false });
expect(discovery.capabilities!.cron).toEqual({ enabled: false });
Expand All @@ -212,7 +212,6 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>
const discovery = await protocol.getDiscovery();

expect(discovery.capabilities).toBeDefined();
expect(discovery.capabilities!.feed).toEqual({ enabled: false });
expect(discovery.capabilities!.comments).toEqual({ enabled: false });
expect(discovery.capabilities!.automation).toEqual({ enabled: false });
expect(discovery.capabilities!.cron).toEqual({ enabled: false });
Expand All @@ -223,21 +222,34 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () =>

it('should dynamically set capabilities based on registered services', async () => {
const mockServices = new Map<string, any>();
mockServices.set('feed', {});
mockServices.set('automation', {});
mockServices.set('search', {});
mockServices.set('file-storage', {});

protocol = new ObjectStackProtocolImplementation(engine, () => mockServices);
const discovery = await protocol.getDiscovery();

expect(discovery.capabilities!.feed).toEqual({ enabled: true });
expect(discovery.capabilities!.comments).toEqual({ enabled: true });
expect(discovery.capabilities!.automation).toEqual({ enabled: true });
expect(discovery.capabilities!.cron).toEqual({ enabled: false });
expect(discovery.capabilities!.search).toEqual({ enabled: true });
expect(discovery.capabilities!.export).toEqual({ enabled: true });
expect(discovery.capabilities!.chunkedUpload).toEqual({ enabled: true });
// comments is independent of services — it tracks the sys_comment object (#3180).
expect(discovery.capabilities!.comments).toEqual({ enabled: false });
});

it('should enable comments capability when the sys_comment object is registered (#3180)', async () => {
// comments/chatter are served by the sys_comment object via the data API
// (ADR-0052 §5), not a dedicated service — so the capability tracks that
// object's presence, keeping declared === enforced.
engine.registerObject(
{ name: 'sys_comment', label: 'Comment', fields: { body: { type: 'text' } } } as any,
'plugin-audit',
);
protocol = new ObjectStackProtocolImplementation(engine, () => new Map());
const discovery = await protocol.getDiscovery();

expect(discovery.capabilities!.comments).toEqual({ enabled: true });
});

it('should enable cron capability when job service is registered', async () => {
Expand Down
12 changes: 3 additions & 9 deletions packages/spec/src/api/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ describe('ApiRoutesSchema', () => {
actions: '/api/v1/p',
storage: '/api/v1/storage',
graphql: '/api/v1/graphql',
feed: '/api/v1/feed',
discovery: '/api/v1/discovery',
});

expect(routes.data).toBe('/api/v1/data');
expect(routes.graphql).toBe('/api/v1/graphql');
expect(routes.feed).toBe('/api/v1/feed');
expect(routes.discovery).toBe('/api/v1/discovery');
});

Expand Down Expand Up @@ -705,7 +703,6 @@ describe('DiscoverySchema (schemaDiscovery field)', () => {
describe('WellKnownCapabilitiesSchema', () => {
it('should accept all capabilities enabled', () => {
const caps: WellKnownCapabilities = {
feed: true,
comments: true,
automation: true,
cron: true,
Expand All @@ -718,27 +715,25 @@ describe('WellKnownCapabilitiesSchema', () => {

it('should accept all capabilities disabled', () => {
const caps = WellKnownCapabilitiesSchema.parse({
feed: false,
comments: false,
automation: false,
cron: false,
search: false,
export: false,
chunkedUpload: false,
});
expect(caps.feed).toBe(false);
expect(caps.comments).toBe(false);
expect(caps.chunkedUpload).toBe(false);
});

it('should reject missing required fields', () => {
expect(() => WellKnownCapabilitiesSchema.parse({ feed: true })).toThrow();
expect(() => WellKnownCapabilitiesSchema.parse({ comments: true })).toThrow();
expect(() => WellKnownCapabilitiesSchema.parse({})).toThrow();
});

it('should reject non-boolean values', () => {
expect(() => WellKnownCapabilitiesSchema.parse({
feed: 'yes',
comments: true,
comments: 'yes',
automation: true,
cron: true,
search: true,
Expand All @@ -749,7 +744,6 @@ describe('WellKnownCapabilitiesSchema', () => {

it('should have .describe() annotations on all fields', () => {
const shape = WellKnownCapabilitiesSchema.shape;
expect(shape.feed.description).toBeDefined();
expect(shape.comments.description).toBeDefined();
expect(shape.automation.description).toBeDefined();
expect(shape.cron.description).toBeDefined();
Expand Down
9 changes: 2 additions & 7 deletions packages/spec/src/api/discovery.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,6 @@ export const ApiRoutesSchema = lazySchema(() => z.object({

/** Base URL for Internationalization */
i18n: z.string().optional().describe('e.g. /api/v1/i18n'),

/** Base URL for Feed / Chatter API */
feed: z.string().optional().describe('e.g. /api/v1/feed'),
}));

/**
Expand Down Expand Up @@ -280,10 +277,8 @@ export const DiscoverySchema = lazySchema(() => z.object({
* Clients can use these to show/hide UI elements without probing individual endpoints.
*/
export const WellKnownCapabilitiesSchema = lazySchema(() => z.object({
/** Whether the backend supports Feed / Chatter API */
feed: z.boolean().describe('Whether the backend supports Feed / Chatter API'),
/** Whether the backend supports comments (a subset of Feed) */
comments: z.boolean().describe('Whether the backend supports comments (a subset of Feed)'),
/** Whether the backend supports record comments / chatter (served by `sys_comment` via the data API) */
comments: z.boolean().describe('Whether the backend supports record comments / chatter (the `sys_comment` object served via the data API)'),
/** Whether the backend supports Automation CRUD (flows, triggers) */
automation: z.boolean().describe('Whether the backend supports Automation CRUD (flows, triggers)'),
/** Whether the backend supports cron scheduling */
Expand Down
9 changes: 4 additions & 5 deletions packages/spec/src/api/dispatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,12 @@ describe('DEFAULT_DISPATCHER_ROUTES', () => {
expect(services).toContain('auth');
});

it('should include storage and feed services', () => {
it('should include the storage service', () => {
const services = DEFAULT_DISPATCHER_ROUTES.map(r => r.service);
expect(services).toContain('file-storage');
// feed route maps to the data service
const feedRoute = DEFAULT_DISPATCHER_ROUTES.find(r => r.prefix.includes('feed'));
expect(feedRoute).toBeDefined();
expect(feedRoute!.service).toBe('data');
// The feed route was retired (ADR-0052 §5 / #3180) — comments/activity are
// served by the generic data API on sys_comment / sys_activity.
expect(DEFAULT_DISPATCHER_ROUTES.find(r => r.prefix.includes('feed'))).toBeUndefined();
});

it('should include optional services', () => {
Expand Down
1 change: 0 additions & 1 deletion packages/spec/src/api/dispatcher.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ export const DEFAULT_DISPATCHER_ROUTES: DispatcherRouteInput[] = [
{ prefix: '/api/v1/analytics', service: 'analytics' },
{ prefix: '/api/v1/automation', service: 'automation' },
{ prefix: '/api/v1/storage', service: 'file-storage' },
{ prefix: '/api/v1/feed', service: 'data' },
{ prefix: '/api/v1/i18n', service: 'i18n' },
{ prefix: '/api/v1/notifications', service: 'notification' },
{ prefix: '/api/v1/realtime', service: 'realtime' },
Expand Down