From bc70d854f7dcb174809ef00149dedad46fa33994 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 01:55:10 +0000 Subject: [PATCH 1/7] wip(plugin-audit): sys_activity default view, plain description and label Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .../src/objects/sys-activity.object.ts | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index 34bc4a358e4..188fd4d4e49 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -1,6 +1,34 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { ObjectSchema, Field, SYS_ACTIVITY_BUILTIN_TYPES } from '@objectstack/spec/data'; +import { + SysMetadataObject, + SysMetadataHistoryObject, + SysMetadataCommitObject, + SysMetadataAuditObject, +} from '@objectstack/metadata-core'; +import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; + +/** + * The platform's own bookkeeping objects whose rows the default `recent` view + * leaves out: the metadata store (published items, drafts, history, commits, + * the store's own audit) and the data-migration ledger. Their writes are + * mirrored like any other object's, and those rows stay stored and readable: + * the `all` view below lists them, and each one has its compliance row in + * `sys_audit_log`. + * + * Read from the definitions themselves, so a rename moves this list with it. + * Deliberately not a name prefix: `sys_*` also names objects whose rows are + * people's work (a user an admin invites, a file someone uploads). + */ +export const ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS: readonly string[] = Object.freeze([ + SysMetadataObject.name, + SysMetadataHistoryObject.name, + SysMetadataCommitObject.name, + SysMetadataAuditObject.name, + SysMigration.name, + SysMigrationJournal.name, +]); /** * sys_activity — Lightweight Activity Stream @@ -18,8 +46,10 @@ import { ObjectSchema, Field, SYS_ACTIVITY_BUILTIN_TYPES } from '@objectstack/sp */ export const SysActivity = ObjectSchema.create({ name: 'sys_activity', - label: 'Activity', - pluralLabel: 'Activities', + // Not "Activity": an app's own object is often called that (a CRM's calls + // and meetings), and this page sat beside it under the same word. + label: 'Activity Feed', + pluralLabel: 'Activity Feed', icon: 'activity', isSystem: true, managedBy: 'append-only', @@ -32,11 +62,54 @@ export const SysActivity = ObjectSchema.create({ storage: { strategy: 'rotation', shards: 14, unit: 'day' }, reclaim: true, }, - description: 'Recent activity stream entries (lightweight, denormalized)', + description: 'What people did to records recently, newest first: who created, changed or deleted what. Entries are kept for 14 days.', displayNameField: 'summary', nameField: 'summary', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField) highlightFields: ['timestamp', 'type', 'actor_name', 'summary'], + // The list a reader lands on with no view named (Home's "view all activity" + // and the bell's Activity tab both open the bare object route) is the FIRST + // view declared here, so `recent` stays first. + // + // `recent` is the cut Home's activity card makes, as far as a query column + // can say it: a person acted (`actor_id` set; a system write leaves it null, + // ADR-0118 D1: a sign-in's `last_login_at` stamp, a migration, a seed), and + // the row is not about the platform's own bookkeeping + // (`ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS`). It names no value-bearing column + // (`parent-field-query-guard.ts`), so every reader may run it. + // + // `all` is every row the reader may read, platform rows included: where an + // administrator still finds what `recent` leaves out. Nothing is removed from + // storage and nothing changes in what is written. + listViews: { + recent: { + type: 'grid', + name: 'recent', + label: 'Recent activity', + data: { provider: 'object', object: 'sys_activity' }, + columns: ['timestamp', 'actor_name', 'type', 'summary'], + filter: [ + { field: 'actor_id', operator: 'is_not_null' }, + { field: 'object_name', operator: 'not_in', value: [...ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS] }, + ], + sort: [{ field: 'timestamp', order: 'desc' }], + pagination: { pageSize: 50 }, + emptyState: { + title: 'No recent activity', + message: 'Changes people make to records appear here, newest first.', + }, + }, + all: { + type: 'grid', + name: 'all', + label: 'All entries', + data: { provider: 'object', object: 'sys_activity' }, + columns: ['timestamp', 'actor_name', 'type', 'summary'], + sort: [{ field: 'timestamp', order: 'desc' }], + pagination: { pageSize: 100 }, + }, + }, + fields: { id: Field.text({ label: 'Activity ID', From 38c94c6cf11afcb242561e1f9d45813930b720d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 02:02:52 +0000 Subject: [PATCH 2/7] fix(plugin-audit): activity list opens newest first, without platform rows, under a plain name Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .../30036-activity-feed-default-view.md | 11 + ...-activity-default-view.integration.test.ts | 190 ++++++++++++++++++ .../src/translations/en.objects.generated.ts | 18 +- .../translations/es-ES.objects.generated.ts | 18 +- .../translations/ja-JP.objects.generated.ts | 18 +- .../translations/zh-CN.objects.generated.ts | 18 +- 6 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 .changeset/30036-activity-feed-default-view.md create mode 100644 packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts diff --git a/.changeset/30036-activity-feed-default-view.md b/.changeset/30036-activity-feed-default-view.md new file mode 100644 index 00000000000..ec35ca4c881 --- /dev/null +++ b/.changeset/30036-activity-feed-default-view.md @@ -0,0 +1,11 @@ +--- +'@objectstack/plugin-audit': patch +--- + +fix(plugin-audit): the activity list opens newest first, without the platform's own rows, under a plain name and description + +Clause-②: no +Metadata-protocol: none + +- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own bookkeeping (the metadata store objects and the data-migration ledger). Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. +- **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. diff --git a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts new file mode 100644 index 00000000000..37cad385eb9 --- /dev/null +++ b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts @@ -0,0 +1,190 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * `sys_activity`'s default list: what a reader sees when Home's "view all + * activity" or the bell's Activity tab opens the bare object route. + * + * Both doors name no view, so the Console opens the FIRST declared list view. + * That view (`recent`) must answer newest first, and must leave out the rows + * about the platform's own bookkeeping (the metadata store, the migration + * ledger) and the rows no person wrote (a sign-in stamp, a migration, a seed: + * the system actor is `null`, ADR-0118 D1). The second view (`all`) still lists + * every row, which is where an administrator finds the rest. + * + * ## Why a real engine and a real driver + * + * The view is a filter and a sort the Console sends to the server. Whether + * that filter selects the right rows is a question about the Filter Protocol + * as a SQL driver executes it (`$null`, `$nin`), so the view's own rules are + * lowered the way the Console lowers a view filter (one `[field, operator, + * value]` node per rule under `and`) and parsed by the server's own + * `parseFilterAST`, then run through the real engine on SQLite. The human and + * system rows on a business object are written by the real audit writer, so + * the "system write leaves `actor_id` null" premise is the writer's, not the + * fixture's. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { ObjectKernel } from '@objectstack/core'; +import { ObjectQL, ObjectQLPlugin } from '@objectstack/objectql'; +import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; +import { parseFilterAST } from '@objectstack/spec/data'; +import { + SysMetadataObject, + SysMetadataHistoryObject, + SysMetadataCommitObject, + SysMetadataAuditObject, +} from '@objectstack/metadata-core'; +import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; + +import { AuditPlugin } from './audit-plugin.js'; +import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js'; +import { installDefaultOrganization } from './default-organization.testkit.js'; +import { SysActivity } from './objects/index.js'; +import { ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS } from './objects/sys-activity.object.js'; + +const ACTIVITY = 'sys_activity'; +const DEAL = 'act_deal'; +const HARNESS_PACKAGE = 'com.objectstack.audit.test.activity-default-view'; + +const SYS = { isSystem: true } as const; +const ADMIN = { userId: 'u_admin', tenantId: 'org_default', positions: ['org_admin'] }; + +const dealObject = { + name: DEAL, + label: 'Deal', + fields: { name: { name: 'name', label: 'Name', type: 'text' as const } }, +}; + +type Row = Record; +type ViewRule = { field: string; operator: string; value?: unknown }; +type ListView = { filter?: ViewRule[]; sort?: Array<{ field: string; order: 'asc' | 'desc' }> }; + +const views = (SysActivity as unknown as { listViews: Record }).listViews; + +/** The Console's lowering of a view filter: one node per rule, under `and`. */ +function whereOf(view: ListView): Record | undefined { + const rules = view.filter ?? []; + if (rules.length === 0) return undefined; + return parseFilterAST(['and', ...rules.map((r) => [r.field, r.operator, r.value])]) as Record; +} + +describe('sys_activity default list view — the declaration', () => { + it('the first declared view is `recent`, the one a door with no view name opens', () => { + expect(Object.keys(views)[0]).toBe('recent'); + }); + + it('both views read newest first', () => { + expect(views.recent.sort).toEqual([{ field: 'timestamp', order: 'desc' }]); + expect(views.all.sort).toEqual([{ field: 'timestamp', order: 'desc' }]); + }); + + it('the excluded objects are read from the definitions they name', () => { + expect([...ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS].sort()).toEqual( + [ + SysMetadataObject.name, + SysMetadataHistoryObject.name, + SysMetadataCommitObject.name, + SysMetadataAuditObject.name, + SysMigration.name, + SysMigrationJournal.name, + ].sort(), + ); + }); + + it('`all` carries no filter, so every readable row stays one click away', () => { + expect(views.all.filter).toBeUndefined(); + }); + + it('the description is not the developer sentence the page used to show', () => { + expect(SysActivity.description).not.toMatch(/denormalized/i); + expect(SysActivity.label).not.toBe('Activity'); + }); +}); + +describe('sys_activity default list view — the rows it serves', () => { + let kernel: ObjectKernel; + let engine: ObjectQL; + const summaries = (rows: Row[]) => rows.map((r) => r.summary); + + beforeAll(async () => { + kernel = new ObjectKernel({ logger: { level: 'silent' } }); + await kernel.use(new ObjectQLPlugin()); + await kernel.use(singlePostureTenancy()); + await kernel.use(new AuditPlugin()); + await kernel.bootstrap(); + + engine = kernel.getService('objectql'); + const driver = new SqliteWasmDriver({ filename: ':memory:' }); + await driver.connect(); + engine.registerDriver(driver, true); + engine.registry.registerObject(dealObject as any, HARNESS_PACKAGE); + await engine.syncSchemas(); + await installDefaultOrganization(engine); + + // The real CRUD mirror: a person's write and a system write on a business object. + await engine.insert(DEAL, { name: 'person deal' }, { context: ADMIN }); + await engine.insert(DEAL, { name: 'seeded deal' }, { context: SYS }); + + // Rows in the shapes the platform's own writers leave, at known instants. + const at = (minute: number) => new Date(Date.UTC(2026, 10, 4, 16, minute)).toISOString(); + const rows: Row[] = [ + { summary: 'person edits a deal (16:44)', object_name: DEAL, record_id: 'd1', actor_id: 'u_admin', timestamp: at(44) }, + { summary: 'person creates a deal (16:34)', object_name: DEAL, record_id: 'd2', actor_id: 'u_admin', timestamp: at(34) }, + { summary: 'person renames a user (16:40)', object_name: 'sys_user', record_id: 'u1', actor_id: 'u_admin', timestamp: at(40) }, + { summary: 'sign-in stamp', object_name: 'sys_user', record_id: 'u1', actor_id: null, timestamp: at(45) }, + { summary: 'metadata publish', object_name: SysMetadataObject.name, record_id: 'm1', actor_id: 'u_admin', timestamp: at(46) }, + { summary: 'metadata history', object_name: SysMetadataHistoryObject.name, record_id: 'h1', actor_id: 'u_admin', timestamp: at(47) }, + { summary: 'migration', object_name: SysMigration.name, record_id: 'g1', actor_id: null, timestamp: at(48) }, + ]; + for (const row of rows) { + await engine.insert(ACTIVITY, { type: 'updated', ...row }, { context: SYS }); + } + }); + + afterAll(async () => { + await kernel?.shutdown?.(); + }); + + it('a system write on a business object leaves actor_id null; a person\'s carries the person', async () => { + const mirrored = (await engine.find(ACTIVITY, { + where: { object_name: DEAL, record_id: { $nin: ['d1', 'd2'] } }, + context: SYS, + })) as Row[]; + const byActor = mirrored.map((r) => r.actor_id ?? null).sort(); + expect(byActor).toEqual(['u_admin', null].sort()); + }); + + it('`recent` serves only what people did to records, newest first', async () => { + const recent = (await engine.find(ACTIVITY, { + where: whereOf(views.recent), + orderBy: views.recent.sort, + context: SYS, + })) as Row[]; + const fixed = summaries(recent).filter((s) => / \(16:\d\d\)$/.test(s)); + expect(fixed).toEqual([ + 'person edits a deal (16:44)', + 'person renames a user (16:40)', + 'person creates a deal (16:34)', + ]); + // The mirrored person row is in, the seeded one is not. + expect(recent.filter((r) => r.object_name === DEAL && !/ \(16:/.test(r.summary))).toHaveLength(1); + for (const row of recent) { + expect(row.actor_id).toBeTruthy(); + expect(ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS).not.toContain(row.object_name); + } + const stamps = recent.map((r) => Date.parse(r.timestamp)); + expect(stamps).toEqual([...stamps].sort((a, b) => b - a)); + }); + + it('`all` still lists the rows `recent` leaves out', async () => { + const all = summaries((await engine.find(ACTIVITY, { + where: whereOf(views.all), + orderBy: views.all.sort, + context: SYS, + })) as Row[]); + for (const left of ['sign-in stamp', 'metadata publish', 'metadata history', 'migration']) { + expect(all).toContain(left); + } + }); +}); diff --git a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts index a978e8ddc22..810dfa59d2f 100644 --- a/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/en.objects.generated.ts @@ -113,9 +113,9 @@ export const enObjects: NonNullable = { } }, sys_activity: { - label: "Activity", - pluralLabel: "Activities", - description: "Recent activity stream entries (lightweight, denormalized)", + label: "Activity Feed", + pluralLabel: "Activity Feed", + description: "What people did to records recently, newest first: who created, changed or deleted what. Entries are kept for 14 days.", fields: { id: { label: "Activity ID" @@ -193,6 +193,18 @@ export const enObjects: NonNullable = { label: "Milestone Key", help: "For a fired activity milestone, one sorted, comma-joined key of every field the object's activityMilestones declaration watched when it fired; NULL for every other row. Stamped by the platform at write time and never served: the read rule compares it with the current declaration." } + }, + _views: { + recent: { + label: "Recent activity", + emptyState: { + title: "No recent activity", + message: "Changes people make to records appear here, newest first." + } + }, + all: { + label: "All entries" + } } }, sys_comment: { diff --git a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts index fc324f7d0ab..74682c682fa 100644 --- a/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/es-ES.objects.generated.ts @@ -113,9 +113,9 @@ export const esESObjects: NonNullable = { } }, sys_activity: { - label: "Actividad", - pluralLabel: "Actividades", - description: "Entradas recientes del flujo de actividad (ligeras y desnormalizadas).", + label: "Feed de actividad", + pluralLabel: "Feed de actividad", + description: "Lo que las personas hicieron recientemente en los registros, de lo más nuevo a lo más antiguo: quién creó, cambió o eliminó qué. Las entradas se conservan 14 días.", fields: { id: { label: "ID de actividad" @@ -193,6 +193,18 @@ export const esESObjects: NonNullable = { label: "Clave de hito", help: "Para un hito de actividad disparado, una clave ordenada y separada por comas de todos los campos que la declaración activityMilestones del objeto vigilaba al dispararse; NULL para cualquier otra fila. La plataforma la marca al escribir y nunca se sirve: la regla de lectura la compara con la declaración actual." } + }, + _views: { + recent: { + label: "Actividad reciente", + emptyState: { + title: "Sin actividad reciente", + message: "Los cambios que las personas hacen en los registros aparecen aquí, de lo más nuevo a lo más antiguo." + } + }, + all: { + label: "Todas las entradas" + } } }, sys_comment: { diff --git a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts index 345f3ff150f..67ac92a1168 100644 --- a/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/ja-JP.objects.generated.ts @@ -113,9 +113,9 @@ export const jaJPObjects: NonNullable = { } }, sys_activity: { - label: "アクティビティ", - pluralLabel: "アクティビティ", - description: "最近のアクティビティストリームエントリ(軽量、非正規化)", + label: "アクティビティフィード", + pluralLabel: "アクティビティフィード", + description: "最近レコードに対して行われた操作を新しい順に表示します:誰が何を作成、変更、削除したか。エントリは 14 日間保持されます。", fields: { id: { label: "アクティビティ ID" @@ -193,6 +193,18 @@ export const jaJPObjects: NonNullable = { label: "マイルストーンキー", help: "発火したアクティビティマイルストーンについて、発火時にオブジェクトの activityMilestones 宣言が監視していた全フィールドをソートしてカンマで連結したキー。その他の行では NULL。書き込み時にプラットフォームが付与し、決して返されません。読み取りルールが現在の宣言と比較します。" } + }, + _views: { + recent: { + label: "最近のアクティビティ", + emptyState: { + title: "最近のアクティビティはありません", + message: "レコードへの変更がここに新しい順で表示されます。" + } + }, + all: { + label: "すべてのエントリ" + } } }, sys_comment: { diff --git a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts index 9763ec148ca..7908b7faf63 100644 --- a/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts @@ -113,9 +113,9 @@ export const zhCNObjects: NonNullable = { } }, sys_activity: { - label: "活动", - pluralLabel: "活动", - description: "最近活动流条目(轻量、去规范化)", + label: "动态", + pluralLabel: "动态", + description: "最近大家对记录做了什么,按时间从新到旧:谁新建、修改或删除了什么。条目保留 14 天。", fields: { id: { label: "活动 ID" @@ -193,6 +193,18 @@ export const zhCNObjects: NonNullable = { label: "里程碑键", help: "对于已触发的活动里程碑,为该对象 activityMilestones 声明在触发时所监视的全部字段的排序、逗号连接的键;其他行为 NULL。由平台在写入时标记且从不返回:读取规则会将其与当前声明进行比较。" } + }, + _views: { + recent: { + label: "最近动态", + emptyState: { + title: "暂无最近动态", + message: "大家对记录所做的更改会按时间从新到旧显示在这里。" + } + }, + all: { + label: "全部条目" + } } }, sys_comment: { From 4dc9ce3cd231ef9d10a86edcdba3d9e6e8b452fb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 02:05:57 +0000 Subject: [PATCH 3/7] fix(plugin-audit): leave sys_user sign-in stamps out of the default activity view Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .../30036-activity-feed-default-view.md | 2 +- .../src/objects/sys-activity.object.ts | 39 ++++++++++++------- ...-activity-default-view.integration.test.ts | 24 +++++++----- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/.changeset/30036-activity-feed-default-view.md b/.changeset/30036-activity-feed-default-view.md index ec35ca4c881..fde887afe55 100644 --- a/.changeset/30036-activity-feed-default-view.md +++ b/.changeset/30036-activity-feed-default-view.md @@ -7,5 +7,5 @@ fix(plugin-audit): the activity list opens newest first, without the platform's Clause-②: no Metadata-protocol: none -- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own bookkeeping (the metadata store objects and the data-migration ledger). Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. +- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and `sys_user`, on which every sign-in stamps the last-login columns under the signing-in person's name. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. - **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index 188fd4d4e49..baeabd9b89a 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -8,26 +8,37 @@ import { SysMetadataAuditObject, } from '@objectstack/metadata-core'; import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; +import { SystemObjectName } from '@objectstack/spec/system'; /** - * The platform's own bookkeeping objects whose rows the default `recent` view - * leaves out: the metadata store (published items, drafts, history, commits, - * the store's own audit) and the data-migration ledger. Their writes are - * mirrored like any other object's, and those rows stay stored and readable: - * the `all` view below lists them, and each one has its compliance row in - * `sys_audit_log`. + * The platform objects whose rows the default `recent` view leaves out, even + * when a person's session wrote them: * - * Read from the definitions themselves, so a rename moves this list with it. - * Deliberately not a name prefix: `sys_*` also names objects whose rows are - * people's work (a user an admin invites, a file someone uploads). + * - the metadata store (published items, drafts, history, commits, the + * store's own audit): publishing in Studio writes these under the + * publisher's name; + * - the data-migration ledger; + * - `sys_user`: every sign-in stamps `last_login_at` / `last_login_ip` on it + * under the signing-in person's own name, and the only column that tells + * that stamp from a profile edit is `changed_fields`, which a filter may + * not name for every reader (`parent-field-query-guard.ts`). + * + * Their writes are mirrored like any other object's, and those rows stay + * stored and readable: the `all` view below lists them, and each one has its + * compliance row in `sys_audit_log`. + * + * Read from the definitions and the protocol's name constants, so a rename + * moves this list with it. Deliberately not a name prefix: `sys_*` also names + * objects whose rows are people's work (a file someone uploads). */ -export const ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS: readonly string[] = Object.freeze([ +export const ACTIVITY_FEED_EXCLUDED_OBJECTS: readonly string[] = Object.freeze([ SysMetadataObject.name, SysMetadataHistoryObject.name, SysMetadataCommitObject.name, SysMetadataAuditObject.name, SysMigration.name, SysMigrationJournal.name, + SystemObjectName.USER, ]); /** @@ -73,9 +84,9 @@ export const SysActivity = ObjectSchema.create({ // // `recent` is the cut Home's activity card makes, as far as a query column // can say it: a person acted (`actor_id` set; a system write leaves it null, - // ADR-0118 D1: a sign-in's `last_login_at` stamp, a migration, a seed), and - // the row is not about the platform's own bookkeeping - // (`ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS`). It names no value-bearing column + // ADR-0118 D1: a migration, a seed, a platform setting written at boot), and + // the row is not about one of the platform objects + // `ACTIVITY_FEED_EXCLUDED_OBJECTS` names. It names no value-bearing column // (`parent-field-query-guard.ts`), so every reader may run it. // // `all` is every row the reader may read, platform rows included: where an @@ -90,7 +101,7 @@ export const SysActivity = ObjectSchema.create({ columns: ['timestamp', 'actor_name', 'type', 'summary'], filter: [ { field: 'actor_id', operator: 'is_not_null' }, - { field: 'object_name', operator: 'not_in', value: [...ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS] }, + { field: 'object_name', operator: 'not_in', value: [...ACTIVITY_FEED_EXCLUDED_OBJECTS] }, ], sort: [{ field: 'timestamp', order: 'desc' }], pagination: { pageSize: 50 }, diff --git a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts index 37cad385eb9..f70d8013f5a 100644 --- a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts +++ b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts @@ -6,10 +6,11 @@ * * Both doors name no view, so the Console opens the FIRST declared list view. * That view (`recent`) must answer newest first, and must leave out the rows - * about the platform's own bookkeeping (the metadata store, the migration - * ledger) and the rows no person wrote (a sign-in stamp, a migration, a seed: - * the system actor is `null`, ADR-0118 D1). The second view (`all`) still lists - * every row, which is where an administrator finds the rest. + * about the platform's own objects (the metadata store, the migration ledger, + * and `sys_user`, whose sign-in stamp is written under the signing-in person's + * own name) and the rows no person wrote (a migration, a seed: the system actor + * is `null`, ADR-0118 D1). The second view (`all`) still lists every row, which + * is where an administrator finds the rest. * * ## Why a real engine and a real driver * @@ -36,12 +37,13 @@ import { SysMetadataAuditObject, } from '@objectstack/metadata-core'; import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; +import { SystemObjectName } from '@objectstack/spec/system'; import { AuditPlugin } from './audit-plugin.js'; import { singlePostureTenancy } from './__tests__/single-posture-tenancy.js'; import { installDefaultOrganization } from './default-organization.testkit.js'; import { SysActivity } from './objects/index.js'; -import { ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS } from './objects/sys-activity.object.js'; +import { ACTIVITY_FEED_EXCLUDED_OBJECTS } from './objects/sys-activity.object.js'; const ACTIVITY = 'sys_activity'; const DEAL = 'act_deal'; @@ -80,7 +82,7 @@ describe('sys_activity default list view — the declaration', () => { }); it('the excluded objects are read from the definitions they name', () => { - expect([...ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS].sort()).toEqual( + expect([...ACTIVITY_FEED_EXCLUDED_OBJECTS].sort()).toEqual( [ SysMetadataObject.name, SysMetadataHistoryObject.name, @@ -88,6 +90,7 @@ describe('sys_activity default list view — the declaration', () => { SysMetadataAuditObject.name, SysMigration.name, SysMigrationJournal.name, + SystemObjectName.USER, ].sort(), ); }); @@ -131,8 +134,9 @@ describe('sys_activity default list view — the rows it serves', () => { const rows: Row[] = [ { summary: 'person edits a deal (16:44)', object_name: DEAL, record_id: 'd1', actor_id: 'u_admin', timestamp: at(44) }, { summary: 'person creates a deal (16:34)', object_name: DEAL, record_id: 'd2', actor_id: 'u_admin', timestamp: at(34) }, - { summary: 'person renames a user (16:40)', object_name: 'sys_user', record_id: 'u1', actor_id: 'u_admin', timestamp: at(40) }, - { summary: 'sign-in stamp', object_name: 'sys_user', record_id: 'u1', actor_id: null, timestamp: at(45) }, + { summary: 'person deletes a deal (16:40)', object_name: DEAL, record_id: 'd3', actor_id: 'u_admin', timestamp: at(40) }, + // Measured on a fresh `dev:crm` boot: the sign-in stamp carries the person who signed in. + { summary: 'sign-in stamp', object_name: 'sys_user', record_id: 'u_admin', actor_id: 'u_admin', timestamp: at(45) }, { summary: 'metadata publish', object_name: SysMetadataObject.name, record_id: 'm1', actor_id: 'u_admin', timestamp: at(46) }, { summary: 'metadata history', object_name: SysMetadataHistoryObject.name, record_id: 'h1', actor_id: 'u_admin', timestamp: at(47) }, { summary: 'migration', object_name: SysMigration.name, record_id: 'g1', actor_id: null, timestamp: at(48) }, @@ -164,14 +168,14 @@ describe('sys_activity default list view — the rows it serves', () => { const fixed = summaries(recent).filter((s) => / \(16:\d\d\)$/.test(s)); expect(fixed).toEqual([ 'person edits a deal (16:44)', - 'person renames a user (16:40)', + 'person deletes a deal (16:40)', 'person creates a deal (16:34)', ]); // The mirrored person row is in, the seeded one is not. expect(recent.filter((r) => r.object_name === DEAL && !/ \(16:/.test(r.summary))).toHaveLength(1); for (const row of recent) { expect(row.actor_id).toBeTruthy(); - expect(ACTIVITY_PLATFORM_BOOKKEEPING_OBJECTS).not.toContain(row.object_name); + expect(ACTIVITY_FEED_EXCLUDED_OBJECTS).not.toContain(row.object_name); } const stamps = recent.map((r) => Date.parse(r.timestamp)); expect(stamps).toEqual([...stamps].sort((a, b) => b - a)); From 61bb8996972cd2ab891b53fc612a9a819648e264 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 02:06:31 +0000 Subject: [PATCH 4/7] test(plugin-audit): exclude the new fixture id from the mirror probe Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .../src/sys-activity-default-view.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts index f70d8013f5a..32dbae7f1a3 100644 --- a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts +++ b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts @@ -152,7 +152,7 @@ describe('sys_activity default list view — the rows it serves', () => { it('a system write on a business object leaves actor_id null; a person\'s carries the person', async () => { const mirrored = (await engine.find(ACTIVITY, { - where: { object_name: DEAL, record_id: { $nin: ['d1', 'd2'] } }, + where: { object_name: DEAL, record_id: { $nin: ['d1', 'd2', 'd3'] } }, context: SYS, })) as Row[]; const byActor = mirrored.map((r) => r.actor_id ?? null).sort(); From b4e35970be336e9566202afb9dc2429da62dd73d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 02:08:45 +0000 Subject: [PATCH 5/7] fix(plugin-audit): leave the signing-key row out of the default activity view Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .changeset/30036-activity-feed-default-view.md | 2 +- .../plugins/plugin-audit/src/objects/sys-activity.object.ts | 5 ++++- .../src/sys-activity-default-view.integration.test.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.changeset/30036-activity-feed-default-view.md b/.changeset/30036-activity-feed-default-view.md index fde887afe55..7e0cd7e420d 100644 --- a/.changeset/30036-activity-feed-default-view.md +++ b/.changeset/30036-activity-feed-default-view.md @@ -7,5 +7,5 @@ fix(plugin-audit): the activity list opens newest first, without the platform's Clause-②: no Metadata-protocol: none -- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and `sys_user`, on which every sign-in stamps the last-login columns under the signing-in person's name. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. +- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and `sys_user`, on which every sign-in stamps the last-login columns under the signing-in person's name, and `sys_jwks`, whose signing key is minted under the first session that needs it. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. - **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index baeabd9b89a..89c1956746b 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -21,7 +21,9 @@ import { SystemObjectName } from '@objectstack/spec/system'; * - `sys_user`: every sign-in stamps `last_login_at` / `last_login_ip` on it * under the signing-in person's own name, and the only column that tells * that stamp from a profile edit is `changed_fields`, which a filter may - * not name for every reader (`parent-field-query-guard.ts`). + * not name for every reader (`parent-field-query-guard.ts`); + * - `sys_jwks`: the token-signing key pair, minted on first use under + * whichever session first needs it (measured: the first sign-in's). * * Their writes are mirrored like any other object's, and those rows stay * stored and readable: the `all` view below lists them, and each one has its @@ -39,6 +41,7 @@ export const ACTIVITY_FEED_EXCLUDED_OBJECTS: readonly string[] = Object.freeze([ SysMigration.name, SysMigrationJournal.name, SystemObjectName.USER, + SystemObjectName.JWKS, ]); /** diff --git a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts index 32dbae7f1a3..cc87a8b01bc 100644 --- a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts +++ b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts @@ -91,6 +91,7 @@ describe('sys_activity default list view — the declaration', () => { SysMigration.name, SysMigrationJournal.name, SystemObjectName.USER, + SystemObjectName.JWKS, ].sort(), ); }); @@ -139,6 +140,7 @@ describe('sys_activity default list view — the rows it serves', () => { { summary: 'sign-in stamp', object_name: 'sys_user', record_id: 'u_admin', actor_id: 'u_admin', timestamp: at(45) }, { summary: 'metadata publish', object_name: SysMetadataObject.name, record_id: 'm1', actor_id: 'u_admin', timestamp: at(46) }, { summary: 'metadata history', object_name: SysMetadataHistoryObject.name, record_id: 'h1', actor_id: 'u_admin', timestamp: at(47) }, + { summary: 'signing key minted', object_name: SystemObjectName.JWKS, record_id: 'k1', actor_id: 'u_admin', timestamp: at(49) }, { summary: 'migration', object_name: SysMigration.name, record_id: 'g1', actor_id: null, timestamp: at(48) }, ]; for (const row of rows) { @@ -187,7 +189,7 @@ describe('sys_activity default list view — the rows it serves', () => { orderBy: views.all.sort, context: SYS, })) as Row[]); - for (const left of ['sign-in stamp', 'metadata publish', 'metadata history', 'migration']) { + for (const left of ['sign-in stamp', 'metadata publish', 'metadata history', 'signing key minted', 'migration']) { expect(all).toContain(left); } }); From 4cd5356dcdde805e98dd37172955d3fadd74c397 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 02:26:01 +0000 Subject: [PATCH 6/7] fix(plugin-audit): keep the activity object file loadable from source alone Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .../src/objects/sys-activity.object.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index 89c1956746b..06324c7f2c2 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -1,12 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { ObjectSchema, Field, SYS_ACTIVITY_BUILTIN_TYPES } from '@objectstack/spec/data'; -import { - SysMetadataObject, - SysMetadataHistoryObject, - SysMetadataCommitObject, - SysMetadataAuditObject, -} from '@objectstack/metadata-core'; import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; import { SystemObjectName } from '@objectstack/spec/system'; @@ -29,15 +23,21 @@ import { SystemObjectName } from '@objectstack/spec/system'; * stored and readable: the `all` view below lists them, and each one has its * compliance row in `sys_audit_log`. * - * Read from the definitions and the protocol's name constants, so a rename - * moves this list with it. Deliberately not a name prefix: `sys_*` also names - * objects whose rows are people's work (a file someone uploads). + * Read from the definitions and the protocol's name constants where this file + * may import them. The three metadata-store names are spelled out: an object + * file is loaded from source on its own by `check:platform-object-tenancy-census`, + * and `@objectstack/metadata-core` publishes its definitions only from its root + * entry, whose graph that loader cannot evaluate. + * `sys-activity-default-view.integration.test.ts` holds the list equal to the + * definitions' own names, so a rename reds there. Deliberately not a name + * prefix: `sys_*` also names objects whose rows are people's work (a file + * someone uploads). */ export const ACTIVITY_FEED_EXCLUDED_OBJECTS: readonly string[] = Object.freeze([ - SysMetadataObject.name, - SysMetadataHistoryObject.name, - SysMetadataCommitObject.name, - SysMetadataAuditObject.name, + SystemObjectName.METADATA, + 'sys_metadata_history', + 'sys_metadata_commit', + 'sys_metadata_audit', SysMigration.name, SysMigrationJournal.name, SystemObjectName.USER, From 2110d932ef9e88a9755ddb81326ee931a8435bd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 03:31:09 +0000 Subject: [PATCH 7/7] fix(plugin-audit): leave the identity domain's rows out of the default activity view Claude-Session: https://claude.ai/code/session_01Utp6CXc6hMgykEtjFYQXjy Co-authored-by: Claude --- .../30036-activity-feed-default-view.md | 2 +- .../src/objects/sys-activity.object.ts | 26 +++++++++++++------ ...-activity-default-view.integration.test.ts | 24 ++++++++++++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/.changeset/30036-activity-feed-default-view.md b/.changeset/30036-activity-feed-default-view.md index 7e0cd7e420d..414811137e6 100644 --- a/.changeset/30036-activity-feed-default-view.md +++ b/.changeset/30036-activity-feed-default-view.md @@ -7,5 +7,5 @@ fix(plugin-audit): the activity list opens newest first, without the platform's Clause-②: no Metadata-protocol: none -- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and `sys_user`, on which every sign-in stamps the last-login columns under the signing-in person's name, and `sys_jwks`, whose signing key is minted under the first session that needs it. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. +- **`sys_activity` declares two list views.** `recent` comes first, so it is the list Home's "view all activity" and the bell's Activity tab open (both name no view): newest first, and only rows a person wrote (`actor_id` set) about something other than the platform's own objects: the metadata store, the data-migration ledger, and every identity object `@objectstack/platform-objects/identity` declares (users, members, organizations, sessions, keys, SCIM): every sign-in stamps the last-login columns on `sys_user` under the signing-in person's own name, the signing key in `sys_jwks` is minted under the first sign-in's session, and adding a person writes `sys_user` and `sys_member` rows that person can read. Sign-in stamps, migrations, seeds and metadata history no longer sit between the business rows; an administrator's edits to users are in `all`. `all` lists every row the reader may read, platform rows included, newest first. Nothing is removed from storage and nothing changes in what is written; every excluded row still has its `sys_audit_log` entry. - **Label and description.** The object reads `Activity Feed` (zh-CN `动态`) instead of `Activity`, which collided with an app's own activity object (a CRM's calls and meetings), and its description says what the page is instead of `Recent activity stream entries (lightweight, denormalized)`. diff --git a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts index 06324c7f2c2..b204eba7f44 100644 --- a/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts +++ b/packages/plugins/plugin-audit/src/objects/sys-activity.object.ts @@ -2,8 +2,17 @@ import { ObjectSchema, Field, SYS_ACTIVITY_BUILTIN_TYPES } from '@objectstack/spec/data'; import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; +import * as IdentityObjects from '@objectstack/platform-objects/identity'; import { SystemObjectName } from '@objectstack/spec/system'; +/** Every object the identity domain declares (users, members, organizations, sessions, keys, SCIM, …). */ +const IDENTITY_OBJECT_NAMES: readonly string[] = (Object.values(IdentityObjects) as unknown[]) + .filter((value): value is { name: string; fields: object } => + !!value && typeof value === 'object' && typeof (value as { name?: unknown }).name === 'string' + && typeof (value as { fields?: unknown }).fields === 'object') + .map((object) => object.name) + .sort(); + /** * The platform objects whose rows the default `recent` view leaves out, even * when a person's session wrote them: @@ -12,12 +21,14 @@ import { SystemObjectName } from '@objectstack/spec/system'; * store's own audit): publishing in Studio writes these under the * publisher's name; * - the data-migration ledger; - * - `sys_user`: every sign-in stamps `last_login_at` / `last_login_ip` on it - * under the signing-in person's own name, and the only column that tells - * that stamp from a profile edit is `changed_fields`, which a filter may - * not name for every reader (`parent-field-query-guard.ts`); - * - `sys_jwks`: the token-signing key pair, minted on first use under - * whichever session first needs it (measured: the first sign-in's). + * - the identity domain, every object `@objectstack/platform-objects/identity` + * declares. Measured on a fresh boot, all under a person's own id: every + * sign-in stamps `last_login_at` / `last_login_ip` on `sys_user`; the + * token-signing key in `sys_jwks` is minted under the first sign-in's + * session; adding a person writes a `sys_user` and a `sys_member` row, which + * that person can then read. The only column that tells a sign-in stamp from + * a profile edit is `changed_fields`, which a filter may not name for every + * reader (`parent-field-query-guard.ts`), so the cut is by object. * * Their writes are mirrored like any other object's, and those rows stay * stored and readable: the `all` view below lists them, and each one has its @@ -40,8 +51,7 @@ export const ACTIVITY_FEED_EXCLUDED_OBJECTS: readonly string[] = Object.freeze([ 'sys_metadata_audit', SysMigration.name, SysMigrationJournal.name, - SystemObjectName.USER, - SystemObjectName.JWKS, + ...IDENTITY_OBJECT_NAMES, ]); /** diff --git a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts index cc87a8b01bc..6f4f45af96c 100644 --- a/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts +++ b/packages/plugins/plugin-audit/src/sys-activity-default-view.integration.test.ts @@ -37,6 +37,7 @@ import { SysMetadataAuditObject, } from '@objectstack/metadata-core'; import { SysMigration, SysMigrationJournal } from '@objectstack/platform-objects/system'; +import * as IdentityObjects from '@objectstack/platform-objects/identity'; import { SystemObjectName } from '@objectstack/spec/system'; import { AuditPlugin } from './audit-plugin.js'; @@ -62,6 +63,11 @@ type Row = Record; type ViewRule = { field: string; operator: string; value?: unknown }; type ListView = { filter?: ViewRule[]; sort?: Array<{ field: string; order: 'asc' | 'desc' }> }; +/** Every object the identity domain exports, read the same way the declaration reads it. */ +const identityNames = Object.values(IdentityObjects) + .filter((v: any) => v && typeof v === 'object' && typeof v.name === 'string' && typeof v.fields === 'object') + .map((v: any) => v.name as string); + const views = (SysActivity as unknown as { listViews: Record }).listViews; /** The Console's lowering of a view filter: one node per rule, under `and`. */ @@ -90,12 +96,23 @@ describe('sys_activity default list view — the declaration', () => { SysMetadataAuditObject.name, SysMigration.name, SysMigrationJournal.name, - SystemObjectName.USER, - SystemObjectName.JWKS, + ...identityNames, ].sort(), ); }); + it('the identity objects measured writing under a person\'s id are among the excluded', () => { + for (const object of [ + IdentityObjects.SysUser, + IdentityObjects.SysMember, + IdentityObjects.SysJwks, + IdentityObjects.SysOrganization, + IdentityObjects.SysInvitation, + ]) { + expect(ACTIVITY_FEED_EXCLUDED_OBJECTS).toContain(object.name); + } + }); + it('`all` carries no filter, so every readable row stays one click away', () => { expect(views.all.filter).toBeUndefined(); }); @@ -140,6 +157,7 @@ describe('sys_activity default list view — the rows it serves', () => { { summary: 'sign-in stamp', object_name: 'sys_user', record_id: 'u_admin', actor_id: 'u_admin', timestamp: at(45) }, { summary: 'metadata publish', object_name: SysMetadataObject.name, record_id: 'm1', actor_id: 'u_admin', timestamp: at(46) }, { summary: 'metadata history', object_name: SysMetadataHistoryObject.name, record_id: 'h1', actor_id: 'u_admin', timestamp: at(47) }, + { summary: 'member added', object_name: IdentityObjects.SysMember.name, record_id: 'mb1', actor_id: 'u_admin', timestamp: at(50) }, { summary: 'signing key minted', object_name: SystemObjectName.JWKS, record_id: 'k1', actor_id: 'u_admin', timestamp: at(49) }, { summary: 'migration', object_name: SysMigration.name, record_id: 'g1', actor_id: null, timestamp: at(48) }, ]; @@ -189,7 +207,7 @@ describe('sys_activity default list view — the rows it serves', () => { orderBy: views.all.sort, context: SYS, })) as Row[]); - for (const left of ['sign-in stamp', 'metadata publish', 'metadata history', 'signing key minted', 'migration']) { + for (const left of ['sign-in stamp', 'metadata publish', 'metadata history', 'signing key minted', 'member added', 'migration']) { expect(all).toContain(left); } });