@ossy/workspaces 1.17.1 → 1.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +8 -4
  2. package/src/CreateWorkspacePage.jsx +7 -16
  3. package/src/Definition.js +1 -0
  4. package/src/GeneralSettings.jsx +77 -19
  5. package/src/Invitations.jsx +7 -7
  6. package/src/Invite.jsx +13 -25
  7. package/src/SchemasReadBootstrap.jsx +20 -0
  8. package/src/SelectWorkspacePage.jsx +5 -8
  9. package/src/Users.jsx +7 -7
  10. package/src/Users.page.jsx +3 -3
  11. package/src/accept-invitation.action.js +1 -1
  12. package/src/accept-invitation.api.js +46 -0
  13. package/src/accept-invitation.task.js +6 -5
  14. package/src/create-api-token.action.js +1 -1
  15. package/src/create-api-token.task.js +1 -1
  16. package/src/create.action.js +1 -1
  17. package/src/create.task.js +1 -1
  18. package/src/disable-service.action.js +1 -1
  19. package/src/disable-service.task.js +3 -15
  20. package/src/en.translations.json +36 -30
  21. package/src/enable-service.action.js +1 -1
  22. package/src/enable-service.task.js +3 -15
  23. package/src/entitlements.js +199 -0
  24. package/src/entitlements.spec.js +88 -0
  25. package/src/get-api-tokens.action.js +1 -1
  26. package/src/get-api-tokens.task.js +1 -1
  27. package/src/get-invitations.action.js +1 -1
  28. package/src/get-invitations.task.js +1 -1
  29. package/src/get-schemas.action.js +1 -0
  30. package/src/get-schemas.task.js +14 -0
  31. package/src/get-users.action.js +1 -1
  32. package/src/get-users.task.js +1 -1
  33. package/src/get.action.js +1 -1
  34. package/src/get.task.js +1 -1
  35. package/src/import-schemas.action.js +1 -0
  36. package/src/import-schemas.task.js +26 -0
  37. package/src/index.js +3 -2
  38. package/src/invite-user.action.js +1 -1
  39. package/src/invite-user.task.js +4 -3
  40. package/src/list.action.js +1 -1
  41. package/src/list.task.js +1 -1
  42. package/src/merge-workspace-schemas.js +13 -0
  43. package/src/merge-workspace-schemas.spec.js +16 -0
  44. package/src/remove-member.action.js +1 -1
  45. package/src/remove-member.task.js +1 -1
  46. package/src/schema-ids.js +3 -0
  47. package/src/select-workspace.api.js +50 -0
  48. package/src/selectWorkspace.js +11 -0
  49. package/src/service-keys.js +1 -0
  50. package/src/stories/Users.stories.jsx +35 -0
  51. package/src/stories/decorators.jsx +59 -0
  52. package/src/sv.translations.json +36 -30
  53. package/src/sync-workspace-membership.task.js +7 -6
  54. package/src/workspace-invitation.email.jsx +1 -1
  55. package/src/workspace-users.component.jsx +3 -3
  56. package/src/workspace.aggregate.js +35 -47
  57. package/src/workspace.schema.js +9 -0
  58. package/src/workspaces.events.js +61 -155
  59. package/src/{workspaces.spec.js → workspaces.integration.spec.js} +40 -38
  60. package/src/workspaces.queries.js +27 -12
  61. package/src/get-resource-templates.action.js +0 -1
  62. package/src/get-resource-templates.task.js +0 -18
  63. package/src/import-resource-templates.action.js +0 -1
  64. package/src/import-resource-templates.task.js +0 -26
@@ -8,41 +8,41 @@
8
8
  "workspaces.select.title": "My workspaces",
9
9
  "workspaces.select.newWorkspace": "New workspace",
10
10
  "workspaces.select.loading": "Loading...",
11
- "workspaces.select.description": "Workspaces are separate environments where you can manage resources, templates, services and billing.",
11
+ "workspaces.select.description": "Workspaces are separate environments where you can manage resources, schemas, services, and billing.",
12
12
  "workspaces.select.errorTitle": "We could not load your workspaces",
13
13
  "workspaces.select.errorText": "Something went wrong when loading your workspaces. Maybe your session has expired, or we messed up somehow. Try starting over from our home page.",
14
14
  "workspaces.select.errorAction": "Go to our home page",
15
15
  "workspaces.select.emptyTitle": "Let's get the party started",
16
- "workspaces.select.emptyText": "To get started we need to create a workspace. A workspace is an encapsulation of resources, resource templates, billing information, and what users have access to these things.",
16
+ "workspaces.select.emptyText": "To get started we need to create a workspace. A workspace is an encapsulation of resources, schemas, billing information, and what users have access to these things.",
17
17
  "workspaces.select.emptyAction": "Create a workspace",
18
- "workspaces/create.label": "Create workspace",
19
- "workspaces/create.description": "Create a new workspace for the authenticated user",
20
- "workspaces/accept-invitation.label": "Accept invitation",
21
- "workspaces/accept-invitation.description": "Accept a workspace invitation",
22
- "workspaces/create-api-token.label": "Create workspace API token",
23
- "workspaces/create-api-token.description": "Create an API token scoped to the workspace",
24
- "workspaces/disable-service.label": "Disable service",
25
- "workspaces/disable-service.description": "Disable a service for the workspace",
26
- "workspaces/enable-service.label": "Enable service",
27
- "workspaces/enable-service.description": "Enable a service for the workspace",
28
- "workspaces/get-api-tokens.label": "Get workspace API tokens",
29
- "workspaces/get-api-tokens.description": "List API tokens for the workspace",
30
- "workspaces/get-invitations.label": "Get invitations",
31
- "workspaces/get-invitations.description": "List pending workspace invitations",
32
- "workspaces/get-resource-templates.label": "Get resource templates",
33
- "workspaces/get-resource-templates.description": "List resource templates available to the workspace",
34
- "workspaces/get-users.label": "Get workspace users",
35
- "workspaces/get-users.description": "List members of the workspace",
36
- "workspaces/get.label": "Get workspace",
37
- "workspaces/get.description": "Load a workspace by id",
38
- "workspaces/import-resource-templates.label": "Import resource templates",
39
- "workspaces/import-resource-templates.description": "Import resource templates into the workspace",
40
- "workspaces/invite-user.label": "Invite user",
41
- "workspaces/invite-user.description": "Send a workspace invitation to a user",
42
- "workspaces/remove-member.label": "Remove member",
43
- "workspaces/remove-member.description": "Remove a user from the workspace",
44
- "workspaces/list.label": "List workspaces",
45
- "workspaces/list.description": "List workspaces for the authenticated user",
18
+ "@ossy/workspaces/actions/create.label": "Create workspace",
19
+ "@ossy/workspaces/actions/create.description": "Create a new workspace for the authenticated user",
20
+ "@ossy/workspaces/actions/accept-invitation.label": "Accept invitation",
21
+ "@ossy/workspaces/actions/accept-invitation.description": "Accept a workspace invitation",
22
+ "@ossy/workspaces/actions/create-api-token.label": "Create workspace API token",
23
+ "@ossy/workspaces/actions/create-api-token.description": "Create an API token scoped to the workspace",
24
+ "@ossy/workspaces/actions/disable-service.label": "Disable service",
25
+ "@ossy/workspaces/actions/disable-service.description": "Disable a service for the workspace",
26
+ "@ossy/workspaces/actions/enable-service.label": "Enable service",
27
+ "@ossy/workspaces/actions/enable-service.description": "Enable a service for the workspace",
28
+ "@ossy/workspaces/actions/get-api-tokens.label": "Get workspace API tokens",
29
+ "@ossy/workspaces/actions/get-api-tokens.description": "List API tokens for the workspace",
30
+ "@ossy/workspaces/actions/get-invitations.label": "Get invitations",
31
+ "@ossy/workspaces/actions/get-invitations.description": "List pending workspace invitations",
32
+ "@ossy/workspaces/actions/get-schemas.label": "Get schemas",
33
+ "@ossy/workspaces/actions/get-schemas.description": "List schemas available to the workspace",
34
+ "@ossy/workspaces/actions/get-users.label": "Get workspace users",
35
+ "@ossy/workspaces/actions/get-users.description": "List members of the workspace",
36
+ "@ossy/workspaces/actions/get.label": "Get workspace",
37
+ "@ossy/workspaces/actions/get.description": "Load a workspace by id",
38
+ "@ossy/workspaces/actions/import-schemas.label": "Import schemas",
39
+ "@ossy/workspaces/actions/import-schemas.description": "Import schemas into the workspace",
40
+ "@ossy/workspaces/actions/invite-user.label": "Invite user",
41
+ "@ossy/workspaces/actions/invite-user.description": "Send a workspace invitation to a user",
42
+ "@ossy/workspaces/actions/remove-member.label": "Remove member",
43
+ "@ossy/workspaces/actions/remove-member.description": "Remove a user from the workspace",
44
+ "@ossy/workspaces/actions/list.label": "List workspaces",
45
+ "@ossy/workspaces/actions/list.description": "List workspaces for the authenticated user",
46
46
  "workspace/settings.documentTitle": "Workspace settings",
47
47
  "workspace.settings.title": "Workspace settings",
48
48
  "workspace/users.documentTitle": "Users",
@@ -54,10 +54,16 @@
54
54
  "workspace/invitations/add.documentTitle": "Invite user",
55
55
  "workspace.invitations.title": "Invitations",
56
56
  "workspace.settings.detailsTitle": "Workspace details",
57
+ "workspace.settings.detailsDescription": "Manage workspace name, services, and metadata.",
57
58
  "workspace.settings.edit": "Edit",
58
59
  "workspace.settings.name": "Name",
59
60
  "workspace.settings.created": "Created",
60
61
  "workspace.settings.services": "Services",
62
+ "workspace.settings.service.booking": "Booking",
63
+ "workspace.settings.service.on": "on",
64
+ "workspace.settings.service.off": "off",
65
+ "workspace.settings.service.enable": "Enable",
66
+ "workspace.settings.service.disable": "Disable",
61
67
  "workspace.settings.loading": "Loading...",
62
68
  "workspace.invitations.description": "Invite users to your workspace to collaborate.",
63
69
  "workspace.invitations.invite": "Invite user",
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/enable-service', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/enable-service', access: 'workspace' }
@@ -1,25 +1,13 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
2
  import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
3
+ import { assertEnableableService } from './service-keys.js'
3
4
 
4
- export const metadata = { id: 'workspaces/enable-service' }
5
-
6
- const AVAILABLE_SERVICES = [
7
- '@ossy/tasks/visual-content-descriptors',
8
- '@ossy/tasks/resize-common-web',
9
- '@ossy/resumes',
10
- '@ossy/consultancy',
11
- '@ossy/apps',
12
- '@ossy/domains',
13
- ]
5
+ export const metadata = { id: '@ossy/workspaces/tasks/enable-service' }
14
6
 
15
7
  export async function run({ payload, req }) {
16
8
  const createdBy = payload?.userId ?? req?.userId
17
9
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
18
- const service = payload?.service ?? payload
19
-
20
- if (!AVAILABLE_SERVICES.includes(service)) {
21
- throw Object.assign(new Error(`Service ${service} is not available`), { status: 400 })
22
- }
10
+ const service = assertEnableableService(payload?.service ?? payload)
23
11
 
24
12
  const event = WorkspacesEvents.ServiceEnabled({ createdBy, service })
25
13
  await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Workspace entitlements — canonical helpers (ENTITLEMENTS.md, ADR 0010 Wave 1).
3
+ *
4
+ * Service keys use npm package names (e.g. `@ossy/booking`, `@acme/widgets`) so
5
+ * third-party feature packages work without slug conventions.
6
+ */
7
+
8
+ /** Platform packages that are never workspace-toggleable products. */
9
+ const NON_PRODUCT_PACKAGES = new Set([
10
+ '@ossy/app-test',
11
+ '@ossy/platform',
12
+ '@ossy/app',
13
+ '@ossy/workspaces',
14
+ ])
15
+
16
+ /** @type {Set<string> | null} */
17
+ let enableablePackages = null
18
+
19
+ /**
20
+ * Register packages that may be enabled/disabled at runtime (from app manifest at startup).
21
+ *
22
+ * @param {Iterable<string>} packages
23
+ */
24
+ export function setEnableablePackages (packages) {
25
+ if (packages == null) {
26
+ enableablePackages = null
27
+ return
28
+ }
29
+ enableablePackages = packages instanceof Set ? packages : new Set(packages)
30
+ }
31
+
32
+ /**
33
+ * @param {string} packageName npm package name (e.g. `@ossy/booking`).
34
+ * @returns {string} URL slug (e.g. `booking`).
35
+ */
36
+ export function packageNameToSlug (packageName) {
37
+ const slash = packageName.indexOf('/')
38
+ return slash === -1 ? packageName : packageName.slice(slash + 1)
39
+ }
40
+
41
+ /**
42
+ * @param {unknown} entry
43
+ * @returns {boolean}
44
+ */
45
+ export function entryEnabled (entry) {
46
+ if (entry != null && typeof entry === 'object') return entry.enabled === true
47
+ return false
48
+ }
49
+
50
+ /**
51
+ * Canonicalize `workspace.services` — npm package keys and `{ enabled }` values only.
52
+ *
53
+ * @param {Record<string, { enabled?: boolean }> | undefined} services
54
+ * @returns {Record<string, { enabled: boolean }>}
55
+ */
56
+ export function normalizeWorkspaceServices (services) {
57
+ /** @type {Record<string, { enabled: boolean }>} */
58
+ const out = {}
59
+ for (const [packageName, val] of Object.entries(services || {})) {
60
+ if (!packageName.startsWith('@')) continue
61
+ if (val == null || typeof val !== 'object') continue
62
+ out[packageName] = { enabled: val.enabled === true }
63
+ }
64
+ return out
65
+ }
66
+
67
+ /**
68
+ * @param {Record<string, { enabled?: boolean }> | undefined} services
69
+ * @param {string} packageName scoped npm package name (e.g. `@ossy/booking`)
70
+ * @returns {boolean}
71
+ */
72
+ export function isServiceEntitled (services, packageName) {
73
+ const normalized = normalizeWorkspaceServices(services)
74
+ return normalized[normalizeServicePackageName(packageName)]?.enabled === true
75
+ }
76
+
77
+ /**
78
+ * @param {Record<string, { enabled?: boolean }> | undefined} services
79
+ * @returns {string[]} entitled npm package names
80
+ */
81
+ export function entitledPackages (services) {
82
+ return Object.entries(normalizeWorkspaceServices(services))
83
+ .filter(([, v]) => v.enabled === true)
84
+ .map(([packageName]) => packageName)
85
+ }
86
+
87
+ /**
88
+ * @param {Record<string, { enabled?: boolean }> | undefined} services
89
+ * @returns {string[]} entitled slugs (derived from package names)
90
+ */
91
+ export function entitledSlugs (services) {
92
+ return entitledPackages(services).map(packageNameToSlug)
93
+ }
94
+
95
+ /**
96
+ * @param {unknown} service payload field or `{ service }` wrapper
97
+ * @returns {string} scoped npm package name
98
+ */
99
+ export function normalizeServicePackageName (service) {
100
+ let raw = service
101
+ if (raw != null && typeof raw === 'object' && 'service' in raw) {
102
+ raw = raw.service
103
+ }
104
+ if (typeof raw !== 'string' || !raw.trim()) {
105
+ throw Object.assign(new Error('Service package name is required'), { status: 400 })
106
+ }
107
+ const trimmed = raw.trim()
108
+ if (!trimmed.startsWith('@') || !trimmed.includes('/')) {
109
+ throw Object.assign(
110
+ new Error('Service package name must be a scoped npm name (e.g. @ossy/booking)'),
111
+ { status: 400 },
112
+ )
113
+ }
114
+ return trimmed
115
+ }
116
+
117
+ /** @deprecated use {@link normalizeServicePackageName} */
118
+ export const normalizeServiceSlug = normalizeServicePackageName
119
+
120
+ /**
121
+ * Collect unique npm package names referenced in a build manifest.
122
+ *
123
+ * @param {object} manifest
124
+ * @returns {Set<string>}
125
+ */
126
+ export function collectManifestPackages (manifest) {
127
+ /** @type {Set<string>} */
128
+ const names = new Set()
129
+ const add = (item) => {
130
+ if (item?.package?.startsWith('@')) names.add(item.package)
131
+ }
132
+ for (const page of manifest.pages || []) add(page)
133
+ for (const api of manifest.apis || []) add(api)
134
+ for (const action of manifest.actions || []) add(action)
135
+ for (const task of manifest.tasks || []) add(task)
136
+ for (const task of manifest.taskCatalog || []) add(task)
137
+ for (const schema of manifest.schemas || []) add(schema)
138
+ for (const component of manifest.components || []) add(component)
139
+ for (const aggregate of manifest.aggregates || []) add(aggregate)
140
+ for (const integration of manifest.integrations || []) add(integration)
141
+ for (const email of manifest.emails || []) add(email)
142
+ return names
143
+ }
144
+
145
+ /**
146
+ * Packages installed in this app that may be toggled via ServiceEnabled / ServiceDisabled.
147
+ *
148
+ * @param {object} manifest
149
+ * @returns {Set<string>}
150
+ */
151
+ export function buildEnableablePackageSet (manifest) {
152
+ const definitions = manifest.definitions || {}
153
+ /** @type {Set<string>} */
154
+ const out = new Set()
155
+ for (const packageName of collectManifestPackages(manifest)) {
156
+ const slug = packageNameToSlug(packageName)
157
+ const def = definitions[slug]
158
+ if (isToggleablePackage(packageName, { entitlementRequired: def?.entitlementRequired !== false })) {
159
+ out.add(packageName)
160
+ }
161
+ }
162
+ return out
163
+ }
164
+
165
+ /**
166
+ * @param {string} packageName
167
+ * @param {{ entitlementRequired?: boolean }} [options]
168
+ * @returns {boolean}
169
+ */
170
+ export function isToggleablePackage (packageName, { entitlementRequired = true } = {}) {
171
+ if (entitlementRequired === false) return false
172
+ if (!packageName?.startsWith('@')) return false
173
+ return !NON_PRODUCT_PACKAGES.has(packageName)
174
+ }
175
+
176
+ /**
177
+ * Validate and normalize a service for ServiceEnabled / ServiceDisabled events.
178
+ *
179
+ * @param {unknown} service
180
+ * @param {{ enableablePackages?: Set<string> }} [options]
181
+ * @returns {string} npm package name
182
+ */
183
+ export function assertEnableableService (service, options = {}) {
184
+ const packageName = normalizeServicePackageName(service)
185
+ if (NON_PRODUCT_PACKAGES.has(packageName)) {
186
+ throw Object.assign(new Error(`Service ${packageName} is not enableable`), { status: 400 })
187
+ }
188
+ const allowlist = options.enableablePackages ?? enableablePackages
189
+ if (allowlist && allowlist.size > 0 && !allowlist.has(packageName)) {
190
+ throw Object.assign(
191
+ new Error(`Service ${packageName} is not available in this app`),
192
+ { status: 400, code: 'SERVICE_NOT_AVAILABLE' },
193
+ )
194
+ }
195
+ return packageName
196
+ }
197
+
198
+ /** @deprecated use {@link assertEnableableService} */
199
+ export const assertEnableableServiceSlug = assertEnableableService
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import {
3
+ assertEnableableService,
4
+ buildEnableablePackageSet,
5
+ entitledPackages,
6
+ isServiceEntitled,
7
+ normalizeServicePackageName,
8
+ normalizeWorkspaceServices,
9
+ packageNameToSlug,
10
+ setEnableablePackages,
11
+ } from './entitlements.js'
12
+
13
+ const BOOKING = '@ossy/booking'
14
+ const allowlist = new Set([BOOKING, '@acme/widgets'])
15
+
16
+ describe('entitlements', () => {
17
+ it('packageNameToSlug strips scope', () => {
18
+ expect(packageNameToSlug('@ossy/booking')).toBe('booking')
19
+ expect(packageNameToSlug('@acme/widgets')).toBe('widgets')
20
+ })
21
+
22
+ it('normalizeWorkspaceServices keeps scoped package keys with enabled objects', () => {
23
+ expect(normalizeWorkspaceServices({
24
+ '@ossy/booking': { enabled: true },
25
+ '@acme/widgets': { enabled: false },
26
+ })).toEqual({
27
+ '@ossy/booking': { enabled: true },
28
+ '@acme/widgets': { enabled: false },
29
+ })
30
+ })
31
+
32
+ it('normalizeWorkspaceServices drops slug keys and boolean values', () => {
33
+ expect(normalizeWorkspaceServices({
34
+ booking: { enabled: true },
35
+ '@ossy/booking': true,
36
+ resumes: true,
37
+ })).toEqual({})
38
+ })
39
+
40
+ it('isServiceEntitled requires scoped package name', () => {
41
+ const services = normalizeWorkspaceServices({ '@ossy/booking': { enabled: true } })
42
+ expect(isServiceEntitled(services, BOOKING)).toBe(true)
43
+ expect(() => isServiceEntitled(services, 'booking')).toThrow(/scoped npm name/)
44
+ })
45
+
46
+ it('entitledPackages lists package names', () => {
47
+ const services = normalizeWorkspaceServices({
48
+ '@ossy/booking': { enabled: true },
49
+ '@acme/widgets': { enabled: false },
50
+ })
51
+ expect(entitledPackages(services)).toEqual(['@ossy/booking'])
52
+ })
53
+
54
+ it('normalizeServicePackageName requires scoped npm name', () => {
55
+ expect(normalizeServicePackageName('@acme/widgets')).toBe('@acme/widgets')
56
+ expect(normalizeServicePackageName({ service: BOOKING })).toBe(BOOKING)
57
+ expect(() => normalizeServicePackageName('booking')).toThrow(/scoped npm name/)
58
+ })
59
+
60
+ it('assertEnableableService rejects platform packages', () => {
61
+ expect(() => assertEnableableService('@ossy/workspaces', { enableablePackages: allowlist })).toThrow()
62
+ expect(assertEnableableService(BOOKING, { enableablePackages: allowlist })).toBe(BOOKING)
63
+ })
64
+
65
+ it('assertEnableableService rejects packages not in the app manifest allowlist', () => {
66
+ expect(() => assertEnableableService('@ossy/unknown', { enableablePackages: allowlist })).toThrow(/not available/)
67
+ expect(() => assertEnableableService('booking', { enableablePackages: allowlist })).toThrow(/scoped npm name/)
68
+ })
69
+
70
+ it('assertEnableableService uses setEnableablePackages registry', () => {
71
+ setEnableablePackages(allowlist)
72
+ expect(assertEnableableService('@acme/widgets')).toBe('@acme/widgets')
73
+ expect(() => assertEnableableService('@ossy/domains')).toThrow(/not available/)
74
+ setEnableablePackages(null)
75
+ })
76
+
77
+ it('buildEnableablePackageSet includes entitled products from manifest', () => {
78
+ const manifest = {
79
+ pages: [{ package: '@ossy/booking', id: 'booking/home' }],
80
+ actions: [{ package: '@ossy/workspaces', id: '@ossy/workspaces/actions/get' }],
81
+ definitions: {
82
+ booking: { entitlementRequired: true },
83
+ workspaces: { entitlementRequired: false },
84
+ },
85
+ }
86
+ expect([...buildEnableablePackageSet(manifest)]).toEqual(['@ossy/booking'])
87
+ })
88
+ })
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/get-api-tokens', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/get-api-tokens', access: 'workspace' }
@@ -1,7 +1,7 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
2
  import { Workspace } from '@ossy/workspaces/server'
3
3
 
4
- export const metadata = { id: 'workspaces/get-api-tokens' }
4
+ export const metadata = { id: '@ossy/workspaces/tasks/get-api-tokens' }
5
5
 
6
6
  export async function run({ payload, req }) {
7
7
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/get-invitations', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/get-invitations', access: 'workspace' }
@@ -1,7 +1,7 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
2
  import { Workspace } from '@ossy/workspaces/server'
3
3
 
4
- export const metadata = { id: 'workspaces/get-invitations' }
4
+ export const metadata = { id: '@ossy/workspaces/tasks/get-invitations' }
5
5
 
6
6
  export async function run({ payload, req }) {
7
7
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
@@ -0,0 +1 @@
1
+ export const metadata = { id: '@ossy/workspaces/actions/get-schemas', access: 'workspace' }
@@ -0,0 +1,14 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Workspace } from '@ossy/workspaces/server'
3
+ import { getSystemSchemas } from '@ossy/platform'
4
+ import { mergeWorkspaceSchemas } from './merge-workspace-schemas.js'
5
+
6
+ export const metadata = { id: '@ossy/workspaces/tasks/get-schemas' }
7
+
8
+ export async function run ({ payload, req }) {
9
+ const workspaceId = payload?.workspaceId ?? req?.workspaceId
10
+
11
+ const workspace = await Aggregate.Of(Workspace, workspaceId).then(Aggregate.View())
12
+ const systemSchemas = getSystemSchemas()
13
+ return mergeWorkspaceSchemas(systemSchemas, workspace.schemas || [])
14
+ }
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/get-users', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/get-users', access: 'workspace' }
@@ -2,7 +2,7 @@ import { Aggregate } from '@ossy/event-store'
2
2
  import { Workspace } from '@ossy/workspaces/server'
3
3
  import { User } from '@ossy/users/server'
4
4
 
5
- export const metadata = { id: 'workspaces/get-users' }
5
+ export const metadata = { id: '@ossy/workspaces/tasks/get-users' }
6
6
 
7
7
  export async function run({ payload, req }) {
8
8
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
package/src/get.action.js CHANGED
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/get', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/get', access: 'workspace' }
package/src/get.task.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
2
  import { Workspace } from '@ossy/workspaces/server'
3
3
 
4
- export const metadata = { id: 'workspaces/get' }
4
+ export const metadata = { id: '@ossy/workspaces/tasks/get' }
5
5
 
6
6
  export async function run({ payload, req }) {
7
7
  const workspaceId = payload?.workspaceId ?? req?.workspaceId
@@ -0,0 +1 @@
1
+ export const metadata = { id: '@ossy/workspaces/actions/import-schemas', access: 'workspace' }
@@ -0,0 +1,26 @@
1
+ import { Aggregate } from '@ossy/event-store'
2
+ import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
3
+ import { validateSchemasForImport } from '@ossy/platform'
4
+
5
+ export const metadata = { id: '@ossy/workspaces/tasks/import-schemas' }
6
+
7
+ export async function run ({ payload, req }) {
8
+ const userId = payload?.userId ?? req?.userId
9
+ const workspaceId = payload?.workspaceId ?? req?.workspaceId
10
+ const schemas = payload?.schemas ?? payload
11
+
12
+ if (!Array.isArray(schemas)) {
13
+ throw Object.assign(new Error('schemas must be an array'), { status: 400 })
14
+ }
15
+
16
+ const reservedIds = new Set([])
17
+ const validation = validateSchemasForImport(schemas, reservedIds)
18
+ if (!validation.ok) {
19
+ throw Object.assign(new Error(validation.message), { status: 400, type: validation.code })
20
+ }
21
+
22
+ const event = WorkspacesEvents.SchemasImported({ createdBy: userId, schemas })
23
+
24
+ await Aggregate.Of(Workspace, workspaceId).then(Aggregate.Add(event))
25
+ return event.payload.schemas
26
+ }
package/src/index.js CHANGED
@@ -8,8 +8,8 @@ export { metadata as GetUsers } from './get-users.action.js'
8
8
  export { metadata as RemoveMember } from './remove-member.action.js'
9
9
  export { metadata as CreateWorkspaceApiToken } from './create-api-token.action.js'
10
10
  export { metadata as GetWorkspaceApiTokens } from './get-api-tokens.action.js'
11
- export { metadata as GetResourceTemplates } from './get-resource-templates.action.js'
12
- export { metadata as ImportResourceTemplates } from './import-resource-templates.action.js'
11
+ export { metadata as GetSchemas } from './get-schemas.action.js'
12
+ export { metadata as ImportSchemas } from './import-schemas.action.js'
13
13
  export { metadata as EnableService } from './enable-service.action.js'
14
14
  export { metadata as DisableService } from './disable-service.action.js'
15
15
  export { CreateWorkspacePage } from './CreateWorkspacePage.jsx'
@@ -19,5 +19,6 @@ export { Invitations } from './Invitations.jsx'
19
19
  export { Invite } from './Invite.jsx'
20
20
  export { Users } from './Users.jsx'
21
21
  export { AuthenticationGuard } from './AuthenticationGuard.jsx'
22
+ export { selectWorkspace, selectWorkspaceHref } from './selectWorkspace.js'
22
23
  export { patchUserWorkspaceId } from './patchUserWorkspaceId.js'
23
24
  export { Definition } from './Definition.js'
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/invite-user', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/invite-user', access: 'workspace' }
@@ -1,5 +1,6 @@
1
1
  import jwt from 'jsonwebtoken'
2
2
  import { Aggregate } from '@ossy/event-store'
3
+ import { UserSchema } from '@ossy/users/server'
3
4
  import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
4
5
  import { Token, TokenEvents } from '@ossy/tokens/server'
5
6
  import { EmailRenderer } from '@ossy/email'
@@ -7,9 +8,9 @@ import { ConfigService } from '@ossy/platform'
7
8
  import { createLogger } from '@ossy/observability'
8
9
  import WorkspaceInvitationEmail, { subject as workspaceInvitationSubject } from './workspace-invitation.email.jsx'
9
10
 
10
- export const metadata = { id: 'workspaces/invite-user' }
11
+ export const metadata = { id: '@ossy/workspaces/tasks/invite-user' }
11
12
 
12
- const log = createLogger('workspaces/invite-user')
13
+ const log = createLogger('@ossy/workspaces/tasks/invite-user')
13
14
 
14
15
  const isEmailLike = maybeEmail => /.@.+\.../.test(maybeEmail)
15
16
 
@@ -50,7 +51,7 @@ export async function run({ payload, integrations, req }) {
50
51
  expiresAt,
51
52
  })
52
53
 
53
- const existingUser = await Aggregate.Collection.findOne({ type: 'User', 'state.email': email }).then(agg => agg?.state)
54
+ const existingUser = await Aggregate.Collection.findOne({ type: UserSchema.user, 'state.email': email }).then(agg => agg?.state)
54
55
  if (existingUser && workspace.users.includes(existingUser.id)) {
55
56
  throw Object.assign(new Error('User already in workspace'), { status: 400 })
56
57
  }
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/list', access: 'authenticated' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/list', access: 'authenticated' }
package/src/list.task.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { WorkspacesQueries } from './workspaces.queries.js'
2
2
 
3
- export const metadata = { id: 'workspaces/list' }
3
+ export const metadata = { id: '@ossy/workspaces/tasks/list' }
4
4
 
5
5
  export async function run({ payload, req }) {
6
6
  const userId = payload?.userId ?? req?.userId
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Merge system schemas with workspace overlays (same rules as get-schemas task).
3
+ *
4
+ * @param {object[]} systemSchemas
5
+ * @param {object[]} [workspaceSchemas]
6
+ */
7
+ export function mergeWorkspaceSchemas (systemSchemas, workspaceSchemas = []) {
8
+ const workspaceIds = new Set(workspaceSchemas.map((s) => s?.id).filter(Boolean))
9
+ return [
10
+ ...systemSchemas.filter((s) => !workspaceIds.has(s.id)),
11
+ ...workspaceSchemas,
12
+ ]
13
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import { mergeWorkspaceSchemas } from './merge-workspace-schemas.js'
3
+
4
+ describe('mergeWorkspaceSchemas', () => {
5
+ it('appends workspace-only schemas', () => {
6
+ const system = [{ id: 'a', name: 'A', fields: [] }]
7
+ const workspace = [{ id: 'b', name: 'B', fields: [] }]
8
+ expect(mergeWorkspaceSchemas(system, workspace).map(s => s.id)).toEqual(['a', 'b'])
9
+ })
10
+
11
+ it('lets workspace schemas override system ids', () => {
12
+ const system = [{ id: 'a', name: 'System', fields: [] }]
13
+ const workspace = [{ id: 'a', name: 'Custom', fields: [] }]
14
+ expect(mergeWorkspaceSchemas(system, workspace)[0].name).toBe('Custom')
15
+ })
16
+ })
@@ -1 +1 @@
1
- export const metadata = { id: 'workspaces/remove-member', access: 'workspace' }
1
+ export const metadata = { id: '@ossy/workspaces/actions/remove-member', access: 'workspace' }
@@ -1,7 +1,7 @@
1
1
  import { Aggregate } from '@ossy/event-store'
2
2
  import { Workspace, WorkspacesEvents } from '@ossy/workspaces/server'
3
3
 
4
- export const metadata = { id: 'workspaces/remove-member' }
4
+ export const metadata = { id: '@ossy/workspaces/tasks/remove-member' }
5
5
 
6
6
  export async function run({ payload, req }) {
7
7
  const createdBy = payload?.userId ?? req?.userId
@@ -0,0 +1,3 @@
1
+ export const WorkspaceSchema = Object.freeze({
2
+ workspace: '@ossy/workspaces/schema/workspace',
3
+ })