@ossy/workspaces 1.17.2 → 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.
- package/package.json +8 -4
- package/src/CreateWorkspacePage.jsx +7 -16
- package/src/Definition.js +1 -0
- package/src/GeneralSettings.jsx +77 -19
- package/src/Invitations.jsx +7 -7
- package/src/Invite.jsx +13 -25
- package/src/SchemasReadBootstrap.jsx +20 -0
- package/src/SelectWorkspacePage.jsx +5 -8
- package/src/Users.jsx +7 -7
- package/src/Users.page.jsx +3 -3
- package/src/accept-invitation.action.js +1 -1
- package/src/accept-invitation.api.js +46 -0
- package/src/accept-invitation.task.js +6 -5
- package/src/create-api-token.action.js +1 -1
- package/src/create-api-token.task.js +1 -1
- package/src/create.action.js +1 -1
- package/src/create.task.js +1 -1
- package/src/disable-service.action.js +1 -1
- package/src/disable-service.task.js +3 -15
- package/src/en.translations.json +36 -30
- package/src/enable-service.action.js +1 -1
- package/src/enable-service.task.js +3 -15
- package/src/entitlements.js +199 -0
- package/src/entitlements.spec.js +88 -0
- package/src/get-api-tokens.action.js +1 -1
- package/src/get-api-tokens.task.js +1 -1
- package/src/get-invitations.action.js +1 -1
- package/src/get-invitations.task.js +1 -1
- package/src/get-schemas.action.js +1 -0
- package/src/get-schemas.task.js +14 -0
- package/src/get-users.action.js +1 -1
- package/src/get-users.task.js +1 -1
- package/src/get.action.js +1 -1
- package/src/get.task.js +1 -1
- package/src/import-schemas.action.js +1 -0
- package/src/import-schemas.task.js +26 -0
- package/src/index.js +3 -2
- package/src/invite-user.action.js +1 -1
- package/src/invite-user.task.js +4 -3
- package/src/list.action.js +1 -1
- package/src/list.task.js +1 -1
- package/src/merge-workspace-schemas.js +13 -0
- package/src/merge-workspace-schemas.spec.js +16 -0
- package/src/remove-member.action.js +1 -1
- package/src/remove-member.task.js +1 -1
- package/src/schema-ids.js +3 -0
- package/src/select-workspace.api.js +50 -0
- package/src/selectWorkspace.js +11 -0
- package/src/service-keys.js +1 -0
- package/src/stories/Users.stories.jsx +35 -0
- package/src/stories/decorators.jsx +59 -0
- package/src/sv.translations.json +36 -30
- package/src/sync-workspace-membership.task.js +7 -6
- package/src/workspace-invitation.email.jsx +1 -1
- package/src/workspace-users.component.jsx +3 -3
- package/src/workspace.aggregate.js +35 -47
- package/src/workspace.schema.js +9 -0
- package/src/workspaces.events.js +61 -155
- package/src/{workspaces.spec.js → workspaces.integration.spec.js} +40 -38
- package/src/workspaces.queries.js +27 -12
- package/src/get-resource-templates.action.js +0 -1
- package/src/get-resource-templates.task.js +0 -18
- package/src/import-resource-templates.action.js +0 -1
- package/src/import-resource-templates.task.js +0 -26
package/src/en.translations.json
CHANGED
|
@@ -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,
|
|
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,
|
|
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-
|
|
33
|
-
"workspaces/get-
|
|
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-
|
|
39
|
-
"workspaces/import-
|
|
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
|
+
}
|
package/src/get-users.action.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const metadata = { id: 'workspaces/get-users', access: 'workspace' }
|
|
1
|
+
export const metadata = { id: '@ossy/workspaces/actions/get-users', access: 'workspace' }
|
package/src/get-users.task.js
CHANGED
|
@@ -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
|
|
12
|
-
export { metadata as
|
|
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' }
|
package/src/invite-user.task.js
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/src/list.action.js
CHANGED
|
@@ -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
|