Skip to content

Commit 7cd5645

Browse files
hotlongCopilot
andcommitted
feat(platform-objects): add user_app_state object for @object-ui UI persistence
@object-ui app-shell calls dataSource.find('user_app_state', { filter: { user_id, kind }, limit: 1 }) to persist per-user UI state (grid columns, sidebar collapsed, last-opened tab, etc.). The resource name 'user_app_state' is hard-coded as the default in app-shell, so environments without this object were returning 404 to every Console session. - New ObjectSchema 'user_app_state' (unprefixed to match the @object-ui default) registered via plugin-auth's authIdentityObjects list, so every env that uses plugin-auth (cloud control + every artifact env via ArtifactKernelFactory) gets it automatically. - Member RLS carve-out 'user_app_state_self' filters by user_id = current_user.id, mirroring sys_user_preference_self. The wildcard tenant_isolation policy would otherwise DENY because user_app_state has no organization_id (it's a per-user blob, not per-tenant). - Test updated to include the new policy name and the new system object. Fixes 404 on https://crm.objectos.app/api/v1/data/user_app_state and any other env reported by users. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9d742af commit 7cd5645

5 files changed

Lines changed: 110 additions & 0 deletions

File tree

packages/platform-objects/src/identity/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { SysApiKey } from './sys-api-key.object.js';
2828
export { SysTwoFactor } from './sys-two-factor.object.js';
2929
export { SysDeviceCode } from './sys-device-code.object.js';
3030
export { SysUserPreference } from './sys-user-preference.object.js';
31+
export { UserAppState } from './user-app-state.object.js';
3132

3233
// ── OIDC Provider Objects ──────────────────────────────────────────────────
3334
export { SysOauthApplication } from './sys-oauth-application.object.js';
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { ObjectSchema, Field } from '@objectstack/spec/data';
4+
5+
/**
6+
* user_app_state — Per-user UI app state (set by @object-ui).
7+
*
8+
* Backs the `@object-ui/app-shell` "persistent user state" hook that
9+
* stores per-user, per-kind blobs of UI state (e.g. column widths,
10+
* sidebar collapsed/expanded, last-opened tab) so the Console / any
11+
* @object-ui surface can remember where the user left off.
12+
*
13+
* Contract (consumed by `@object-ui/app-shell` and friends):
14+
* - load(): `find('user_app_state', { filter: { user_id, kind }, limit: 1 })`
15+
* - save(): `create({ user_id, kind, payload, updated_at })` or
16+
* `update(id, { payload, updated_at })`
17+
*
18+
* The object name is intentionally unprefixed (`user_app_state`, not
19+
* `sys_user_app_state`) because `@object-ui` hard-codes the resource
20+
* name as a default; renaming would require every consumer to pass an
21+
* explicit `resource` override.
22+
*
23+
* This is a *system* object — registered automatically with every
24+
* environment by `@objectstack/platform-objects` so the Console works
25+
* out of the box on a fresh env without the user having to install
26+
* anything. Per-user RLS is enforced by the standard owner-row policy
27+
* (`user_id` must equal the calling session's user id) — added in a
28+
* follow-up commit once the policy is finalised.
29+
*/
30+
export const UserAppState = ObjectSchema.create({
31+
name: 'user_app_state',
32+
label: 'User App State',
33+
pluralLabel: 'User App State',
34+
icon: 'settings',
35+
isSystem: true,
36+
managedBy: 'system',
37+
description: 'Per-user UI app state blobs (set by @object-ui app-shell).',
38+
titleFormat: '{kind}',
39+
compactLayout: ['user_id', 'kind'],
40+
41+
fields: {
42+
id: Field.text({
43+
label: 'State ID',
44+
required: true,
45+
readonly: true,
46+
}),
47+
48+
created_at: Field.datetime({
49+
label: 'Created At',
50+
defaultValue: 'NOW()',
51+
readonly: true,
52+
}),
53+
54+
updated_at: Field.datetime({
55+
label: 'Updated At',
56+
defaultValue: 'NOW()',
57+
readonly: true,
58+
}),
59+
60+
user_id: Field.lookup('sys_user', {
61+
label: 'User',
62+
required: true,
63+
description: 'Owner user of this app-state blob.',
64+
}),
65+
66+
kind: Field.text({
67+
label: 'Kind',
68+
required: true,
69+
maxLength: 255,
70+
description: 'State discriminator (e.g. grid-state:account, sidebar).',
71+
}),
72+
73+
payload: Field.json({
74+
label: 'Payload',
75+
description: 'Arbitrary JSON payload — the saved UI state blob.',
76+
}),
77+
},
78+
79+
indexes: [
80+
{ fields: ['user_id', 'kind'], unique: true },
81+
{ fields: ['user_id'], unique: false },
82+
],
83+
84+
enable: {
85+
trackHistory: false,
86+
searchable: false,
87+
apiEnabled: true,
88+
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
89+
trash: false,
90+
mru: false,
91+
},
92+
});

packages/platform-objects/src/platform-objects.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
SysUser,
1515
SysUserPreference,
1616
SysVerification,
17+
UserAppState,
1718
} from './identity/index.js';
1819
import {
1920
SysPermissionSet,
@@ -43,6 +44,7 @@ const systemObjects = [
4344
['SysApiKey', SysApiKey, 'sys_api_key'],
4445
['SysTwoFactor', SysTwoFactor, 'sys_two_factor'],
4546
['SysUserPreference', SysUserPreference, 'sys_user_preference'],
47+
['UserAppState', UserAppState, 'user_app_state'],
4648
['SysRole', SysRole, 'sys_role'],
4749
['SysPermissionSet', SysPermissionSet, 'sys_permission_set'],
4850
['SysUserPermissionSet', SysUserPermissionSet, 'sys_user_permission_set'],
@@ -106,6 +108,7 @@ describe('@objectstack/platform-objects', () => {
106108
'sys_user_preference_self',
107109
'sys_user_self',
108110
'tenant_isolation',
111+
'user_app_state_self',
109112
]);
110113
const tenantPolicy = (member.rowLevelSecurity ?? []).find((p) => p.name === 'tenant_isolation')!;
111114
expect(tenantPolicy.using).toBe('organization_id = current_user.organization_id');

packages/platform-objects/src/security/default-permission-sets.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ export const defaultPermissionSets: PermissionSet[] = [
181181
operation: 'all',
182182
using: 'user_id = current_user.id',
183183
},
184+
{
185+
name: 'user_app_state_self',
186+
object: 'user_app_state',
187+
operation: 'all',
188+
using: 'user_id = current_user.id',
189+
},
184190
{
185191
name: 'sys_api_key_self',
186192
object: 'sys_api_key',
@@ -280,6 +286,12 @@ export const defaultPermissionSets: PermissionSet[] = [
280286
operation: 'select',
281287
using: 'user_id = current_user.id',
282288
},
289+
{
290+
name: 'user_app_state_self',
291+
object: 'user_app_state',
292+
operation: 'select',
293+
using: 'user_id = current_user.id',
294+
},
283295
{
284296
name: 'sys_api_key_self',
285297
object: 'sys_api_key',

packages/plugins/plugin-auth/src/manifest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SysUser,
2828
SysUserPreference,
2929
SysVerification,
30+
UserAppState,
3031
} from '@objectstack/platform-objects/identity';
3132

3233
export const AUTH_PLUGIN_ID = 'com.objectstack.plugin-auth';
@@ -46,6 +47,7 @@ export const authIdentityObjects: any[] = [
4647
SysApiKey,
4748
SysTwoFactor,
4849
SysUserPreference,
50+
UserAppState,
4951
SysOauthApplication,
5052
SysOauthAccessToken,
5153
SysOauthRefreshToken,

0 commit comments

Comments
 (0)