@xpert-ai/plugin-spaces 0.2.0 → 0.3.0

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.
@@ -0,0 +1,7 @@
1
+ import type { EntityManager } from 'typeorm';
2
+ import { SpaceItem } from './entities.js';
3
+ import { type SpaceActor } from './domain.js';
4
+ /** Folder deletion must not silently bypass a resource provider's lifecycle policy.
5
+ * Call under the Space lock when committing a change; the early validation gives
6
+ * the same error before the Markdown move coordinator retires editor rooms. */
7
+ export declare function assertFolderHasNoResources(manager: EntityManager, actor: SpaceActor, item: SpaceItem): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { ConflictException } from '@nestjs/common';
2
+ import { SpaceItem, SpaceResourcePlacement } from './entities.js';
3
+ import { scoped } from './domain.js';
4
+ /** Folder deletion must not silently bypass a resource provider's lifecycle policy.
5
+ * Call under the Space lock when committing a change; the early validation gives
6
+ * the same error before the Markdown move coordinator retires editor rooms. */
7
+ export async function assertFolderHasNoResources(manager, actor, item) {
8
+ if (item.kind !== 'folder')
9
+ return;
10
+ const folders = await manager.getRepository(SpaceItem).findBy({ ...scoped(actor), spaceId: item.spaceId, kind: 'folder' });
11
+ const inside = new Set([item.id]);
12
+ for (let i = 0; i <= folders.length; i++)
13
+ for (const folder of folders) {
14
+ if (folder.parentId && inside.has(folder.parentId))
15
+ inside.add(folder.id);
16
+ }
17
+ const placements = await manager.getRepository(SpaceResourcePlacement).findBy({ ...scoped(actor), spaceId: item.spaceId });
18
+ if (placements.some(placement => placement.parentId && inside.has(placement.parentId))) {
19
+ throw new ConflictException('spaces.folder_contains_resources');
20
+ }
21
+ }
@@ -1,4 +1,5 @@
1
1
  import { type ContentResourceEditor, type ContentResourceRequest, type ContentPrincipal, type ContentPageProvider, type ContentLocationProvider, type ContentPublicationObserver, type ContentReferenceProvider, type IContentResourceProvider } from '@xpert-ai/plugin-sdk';
2
+ import { SpaceItemCatalog } from './item-catalog.js';
2
3
  import { SpaceAccess } from './access.js';
3
4
  import { SpaceFiles } from './files.js';
4
5
  import { SpaceDocuments } from './documents.js';
@@ -12,8 +13,10 @@ export declare class SpacesResourceProvider implements IContentResourceProvider
12
13
  private readonly subpages;
13
14
  private readonly referenceService;
14
15
  private readonly files;
16
+ private readonly itemCatalog;
15
17
  readonly title = "Spaces";
16
- constructor(access: SpaceAccess, documents: SpaceDocuments, knowledge: SpaceKnowledge, subpages: SpaceSubpages, referenceService: SpaceReferences, files: SpaceFiles);
18
+ constructor(access: SpaceAccess, documents: SpaceDocuments, knowledge: SpaceKnowledge, subpages: SpaceSubpages, referenceService: SpaceReferences, files: SpaceFiles, itemCatalog: SpaceItemCatalog);
19
+ get catalog(): import("@xpert-ai/plugin-sdk").ContentCatalogProvider;
17
20
  readonly editor: ContentResourceEditor;
18
21
  readonly references: ContentReferenceProvider;
19
22
  readonly pages: ContentPageProvider;
@@ -9,6 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { ConflictException, Injectable } from '@nestjs/common';
11
11
  import { ContentResourceProvider } from '@xpert-ai/plugin-sdk';
12
+ import { SpaceItemCatalog } from './item-catalog.js';
12
13
  import { Space, SpaceItem } from './entities.js';
13
14
  import { SpaceAccess } from './access.js';
14
15
  import { SpaceFiles } from './files.js';
@@ -17,6 +18,7 @@ import { SpaceKnowledge } from './knowledge.js';
17
18
  import { denied, scoped } from './domain.js';
18
19
  import { SpaceReferences } from './references.js';
19
20
  import { SpaceSubpages } from './subpages.js';
21
+ import { assertFolderHasNoResources } from './resource-folders.js';
20
22
  let SpacesResourceProvider = class SpacesResourceProvider {
21
23
  access;
22
24
  documents;
@@ -24,15 +26,18 @@ let SpacesResourceProvider = class SpacesResourceProvider {
24
26
  subpages;
25
27
  referenceService;
26
28
  files;
29
+ itemCatalog;
27
30
  title = 'Spaces';
28
- constructor(access, documents, knowledge, subpages, referenceService, files) {
31
+ constructor(access, documents, knowledge, subpages, referenceService, files, itemCatalog) {
29
32
  this.access = access;
30
33
  this.documents = documents;
31
34
  this.knowledge = knowledge;
32
35
  this.subpages = subpages;
33
36
  this.referenceService = referenceService;
34
37
  this.files = files;
38
+ this.itemCatalog = itemCatalog;
35
39
  }
40
+ get catalog() { return this.itemCatalog.catalog; }
36
41
  editor = {
37
42
  describe: async (input) => {
38
43
  const actor = { ...input.actor, projectId: input.projectId, assistantId: input.assistantId };
@@ -68,6 +73,8 @@ let SpacesResourceProvider = class SpacesResourceProvider {
68
73
  throw new ConflictException('spaces.version_conflict');
69
74
  if (item.deleted !== input.deleted)
70
75
  await this.access.resource(actor, item.id, 'manage', !input.deleted);
76
+ if (input.deleted)
77
+ await assertFolderHasNoResources(this.access.items.manager, actor, item);
71
78
  const parent = await this.access.resource(actor, input.parentResourceId ?? item.spaceId, 'write');
72
79
  if (parent.space.id !== item.spaceId || (parent.item && parent.item.kind !== 'folder'))
73
80
  throw denied();
@@ -79,7 +86,16 @@ let SpacesResourceProvider = class SpacesResourceProvider {
79
86
  }
80
87
  }
81
88
  async onLocationPrepared(input) {
82
- await this.access.items.update({ ...scoped(input.actor), id: input.source.resourceId, version: input.expectedSourceVersion }, { syncStatus: 'pending', syncError: 'spaces.location_pending' });
89
+ await this.access.items.manager.transaction(async (manager) => {
90
+ const repo = manager.getRepository(SpaceItem);
91
+ const item = await repo.findOneByOrFail({ ...scoped(input.actor), id: input.source.resourceId });
92
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(input.actor), id: item.spaceId }, lock: { mode: 'pessimistic_write' } });
93
+ if (input.deleted)
94
+ await assertFolderHasNoResources(manager, input.actor, item);
95
+ // Resource placement takes the same Space lock and rejects a pending ancestor.
96
+ // Once prepared, no resource can enter a folder while its durable move finishes.
97
+ await repo.update({ ...scoped(input.actor), id: item.id, version: input.expectedSourceVersion }, { syncStatus: 'pending', syncError: 'spaces.location_pending' });
98
+ });
83
99
  }
84
100
  async onLocationChanged(input) {
85
101
  // Projection of a durable authorized operation: it must still finish if the initiating user leaves.
@@ -139,6 +155,6 @@ let SpacesResourceProvider = class SpacesResourceProvider {
139
155
  SpacesResourceProvider = __decorate([
140
156
  Injectable(),
141
157
  ContentResourceProvider('spaces'),
142
- __metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceKnowledge, SpaceSubpages, SpaceReferences, SpaceFiles])
158
+ __metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceKnowledge, SpaceSubpages, SpaceReferences, SpaceFiles, SpaceItemCatalog])
143
159
  ], SpacesResourceProvider);
144
160
  export { SpacesResourceProvider };
package/dist/spaces.js CHANGED
@@ -20,7 +20,9 @@ import { Space, SpaceItem } from './entities.js';
20
20
  import { SpaceAccess } from './access.js';
21
21
  import { denied, id, kindSchema, scoped, title } from './domain.js';
22
22
  import { isInternalSpacePath, SpaceDocuments } from './documents.js';
23
+ import { coveredByContent } from './content-coverage.js';
23
24
  import { SpaceKnowledge } from './knowledge.js';
25
+ import { assertFolderHasNoResources } from './resource-folders.js';
24
26
  export const createItemSchema = z.object({ spaceId: id, parentId: id.nullable().default(null), title,
25
27
  kind: kindSchema, artifactId: id.optional(), artifactVersionId: id.optional(), content: z.string().max(100000).optional() }).strict();
26
28
  export const editItemSchema = z.object({ itemId: id, expectedVersion: z.number().int().positive(),
@@ -57,10 +59,11 @@ let SpacesService = class SpacesService {
57
59
  async tree(actor, resourceId, trash = false) {
58
60
  const principal = await this.access.principal(actor);
59
61
  const root = await this.access.resolve(principal, resourceId, trash ? 'manage' : 'read');
62
+ const coverage = await this.documents.resourceCoverage(actor, root.space);
60
63
  if (!trash)
61
64
  await this.spaces.manager.transaction(async (manager) => {
62
65
  await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: root.space.id }, lock: { mode: 'pessimistic_write' } });
63
- await this.documents.discover(actor, root.space, root.canWrite);
66
+ await this.documents.discover(actor, root.space, root.canWrite, coverage);
64
67
  });
65
68
  const all = await this.access.items.find({ where: { ...scoped(actor), spaceId: root.space.id }, order: { createdAt: 'ASC' } });
66
69
  const items = [];
@@ -78,7 +81,7 @@ let SpacesService = class SpacesService {
78
81
  }
79
82
  for (const item of all) {
80
83
  if (!descendants.has(item.id) || item.deleted !== trash ||
81
- (!trash && item.filePath && isInternalSpacePath(item.filePath)))
84
+ (!trash && item.filePath && (isInternalSpacePath(item.filePath) || coveredByContent(item.filePath, coverage))))
82
85
  continue;
83
86
  try {
84
87
  const result = await this.access.resolve(principal, item.id, 'read', trash);
@@ -152,6 +155,8 @@ let SpacesService = class SpacesService {
152
155
  await this.access.resource(actor, input.itemId, input.deleted !== undefined ? 'manage' : 'write', input.deleted === false);
153
156
  const repo = manager.getRepository(SpaceItem);
154
157
  const item = await repo.findOneByOrFail({ ...scoped(actor), id: input.itemId });
158
+ if (input.deleted === true)
159
+ await assertFolderHasNoResources(manager, actor, item);
155
160
  const previousPath = item.filePath;
156
161
  if (item.version !== input.expectedVersion)
157
162
  throw new ConflictException('spaces.version_conflict');
package/dist/subpages.js CHANGED
@@ -72,7 +72,7 @@ let SpaceSubpages = class SpaceSubpages {
72
72
  item.parentPageId !== parent.id || item.childRequestTitle !== request.title || item.createdById !== actor.userId || item.deleted))
73
73
  throw new ConflictException('spaces.child_request_conflict');
74
74
  if (!item) {
75
- // New IDs cannot make cycles. Bound depth so corrupt legacy metadata cannot loop forever.
75
+ // New IDs cannot make cycles. Bound depth so corrupt metadata cannot loop forever.
76
76
  // Count the child itself as level one, then count its ancestors.
77
77
  let cursor = parent, depth = 1;
78
78
  while (cursor) {
package/dist/view.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SpaceCatalog } from './catalog.js';
1
2
  import { SpaceImports } from './imports.js';
2
3
  import type { XpertExtensionViewManifest, XpertResolvedViewHostContext, XpertViewQuery, XpertViewActionRequest, XpertRemoteComponentViewSchema, XpertViewFileAccessRequest } from '@xpert-ai/contracts';
3
4
  import { type IXpertViewExtensionProvider, type XpertViewFileActionFile } from '@xpert-ai/plugin-sdk';
@@ -10,6 +11,7 @@ import { SpaceKnowledge } from './knowledge.js';
10
11
  import { SpaceContent } from './content.js';
11
12
  import { SpaceAccess } from './access.js';
12
13
  export declare class SpacesView implements IXpertViewExtensionProvider {
14
+ private readonly catalog;
13
15
  private readonly imports;
14
16
  private readonly content;
15
17
  private readonly spaces;
@@ -19,7 +21,7 @@ export declare class SpacesView implements IXpertViewExtensionProvider {
19
21
  private readonly editors;
20
22
  private readonly knowledge;
21
23
  private readonly access;
22
- constructor(imports: SpaceImports, content: SpaceContent, spaces: SpacesService, pages: SpacePages, ai: SpaceAi, files: SpaceFiles, editors: SpaceEditors, knowledge: SpaceKnowledge, access: SpaceAccess);
24
+ constructor(catalog: SpaceCatalog, imports: SpaceImports, content: SpaceContent, spaces: SpacesService, pages: SpacePages, ai: SpaceAi, files: SpaceFiles, editors: SpaceEditors, knowledge: SpaceKnowledge, access: SpaceAccess);
23
25
  supports(context: XpertResolvedViewHostContext): boolean;
24
26
  getViewManifests(context: XpertResolvedViewHostContext, slot: string): XpertExtensionViewManifest[];
25
27
  getRemoteComponentEntry(context: XpertResolvedViewHostContext, viewKey: string, component: XpertRemoteComponentViewSchema['component']): Promise<{
package/dist/view.js CHANGED
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
+ import { SpaceCatalog, catalogQuery, catalogCreate, catalogRename, catalogDuplicate, catalogLifecycle, catalogMove } from './catalog.js';
10
11
  import { SpaceImports, sourceSelection, importSelection } from './imports.js';
11
12
  import { BadRequestException, Injectable } from '@nestjs/common';
12
13
  import { readFile } from 'node:fs/promises';
@@ -23,10 +24,11 @@ import { SpaceContent } from './content.js';
23
24
  import { SpaceAccess } from './access.js';
24
25
  const selection = z.object({ itemId: id }).strict();
25
26
  const label = (en_US, zh_Hans) => ({ en_US, zh_Hans });
26
- const actions = ['source.list', 'source.search', 'source.preview', 'source.import', 'document.open', 'knowledge.enable',
27
+ const actions = ['catalog.duplicate', 'catalog.backup', 'catalog.preferences', 'catalog.preference.set', 'catalog.move', 'catalog.types', 'catalog.list', 'catalog.create', 'catalog.rename', 'catalog.lifecycle', 'source.list', 'source.search', 'source.preview', 'source.import', 'document.open', 'knowledge.enable',
27
28
  'knowledge.status', 'knowledge.refresh', 'knowledge.search', 'item.create', 'item.edit', 'page.read', 'search',
28
29
  'file.list', 'page.export', 'artifact.list', 'artifact.collect', 'artifact.open', 'editor.list', 'editor.create'];
29
30
  let SpacesView = class SpacesView {
31
+ catalog;
30
32
  imports;
31
33
  content;
32
34
  spaces;
@@ -36,7 +38,8 @@ let SpacesView = class SpacesView {
36
38
  editors;
37
39
  knowledge;
38
40
  access;
39
- constructor(imports, content, spaces, pages, ai, files, editors, knowledge, access) {
41
+ constructor(catalog, imports, content, spaces, pages, ai, files, editors, knowledge, access) {
42
+ this.catalog = catalog;
40
43
  this.imports = imports;
41
44
  this.content = content;
42
45
  this.spaces = spaces;
@@ -65,7 +68,7 @@ let SpacesView = class SpacesView {
65
68
  { key: 'editor.import', label: label('Import document', '导入文档'), actionType: 'invoke', transport: 'file' },
66
69
  { key: 'file.upload', label: label('Upload file', '上传文件'), actionType: 'invoke', transport: 'file' }],
67
70
  // Declare both phases: platform resource resolution, then container-local navigation.
68
- clientCommands: ['assistant.context.set', 'workbench.content.resolve', 'workbench.navigation.open'].map((key) => ({ key, required: key === 'workbench.content.resolve', label: label(key, key) })) }];
71
+ clientCommands: ['assistant.chat.send_message', 'assistant.context.set', 'workbench.content.resolve', 'workbench.navigation.open'].map((key) => ({ key, required: key === 'workbench.content.resolve', label: label(key, key) })) }];
69
72
  }
70
73
  async getRemoteComponentEntry(context, viewKey, component) {
71
74
  if (viewKey !== VIEW || component.entry !== 'spaces')
@@ -146,6 +149,36 @@ let SpacesView = class SpacesView {
146
149
  result = await this.knowledge.search(actor, p.spaceId, p.query);
147
150
  break;
148
151
  }
152
+ case 'catalog.backup':
153
+ result = await this.catalog.backup(actor, z.object({ contentId: id }).strict().parse(input).contentId);
154
+ break;
155
+ case 'catalog.preferences':
156
+ result = await this.catalog.preferences(actor, z.object({ contentId: id }).strict().parse(input).contentId);
157
+ break;
158
+ case 'catalog.preference.set':
159
+ result = await this.catalog.setPreference(actor, z.object({ contentId: id, scope: z.enum(['resource', 'project', 'organization']), editorKey: z.string().min(1).max(200).nullable(), expectedRevision: z.number().int().nonnegative() }).strict().parse(input));
160
+ break;
161
+ case 'catalog.move':
162
+ result = await this.catalog.move(actor, catalogMove.parse(input));
163
+ break;
164
+ case 'catalog.types':
165
+ result = await this.catalog.types(actor);
166
+ break;
167
+ case 'catalog.list':
168
+ result = await this.catalog.list(actor, catalogQuery.parse(input));
169
+ break;
170
+ case 'catalog.lifecycle':
171
+ result = await this.catalog.lifecycle(actor, catalogLifecycle.parse(input));
172
+ break;
173
+ case 'catalog.duplicate':
174
+ result = await this.catalog.duplicate(actor, catalogDuplicate.parse(input));
175
+ break;
176
+ case 'catalog.rename':
177
+ result = await this.catalog.rename(actor, catalogRename.parse(input));
178
+ break;
179
+ case 'catalog.create':
180
+ result = await this.catalog.create(actor, catalogCreate.parse(input));
181
+ break;
149
182
  case 'editor.list':
150
183
  result = await this.editors.list(actor);
151
184
  break;
@@ -221,6 +254,6 @@ let SpacesView = class SpacesView {
221
254
  SpacesView = __decorate([
222
255
  Injectable(),
223
256
  ViewExtensionProvider('spaces.views'),
224
- __metadata("design:paramtypes", [SpaceImports, SpaceContent, SpacesService, SpacePages, SpaceAi, SpaceFiles, SpaceEditors, SpaceKnowledge, SpaceAccess])
257
+ __metadata("design:paramtypes", [SpaceCatalog, SpaceImports, SpaceContent, SpacesService, SpacePages, SpaceAi, SpaceFiles, SpaceEditors, SpaceKnowledge, SpaceAccess])
225
258
  ], SpacesView);
226
259
  export { SpacesView };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xpert-ai/plugin-spaces",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -48,8 +48,8 @@
48
48
  "@nestjs/common": "^11.1.6",
49
49
  "@nestjs/core": "^11.1.6",
50
50
  "@nestjs/typeorm": "^11.0.0",
51
- "@xpert-ai/contracts": "^3.27.0",
52
- "@xpert-ai/plugin-sdk": "^3.27.0",
51
+ "@xpert-ai/contracts": "^3.28.0",
52
+ "@xpert-ai/plugin-sdk": "^3.28.0",
53
53
  "typeorm": "^0.3.24",
54
54
  "zod": "3.25.67"
55
55
  },
@@ -62,8 +62,8 @@
62
62
  "@types/node": "^22.13.0",
63
63
  "@types/react": "^18.3.18",
64
64
  "@types/react-dom": "^18.3.5",
65
- "@xpert-ai/contracts": "3.27.0",
66
- "@xpert-ai/plugin-sdk": "3.27.0",
65
+ "@xpert-ai/contracts": "3.28.0",
66
+ "@xpert-ai/plugin-sdk": "3.28.0",
67
67
  "@xpert-ai/plugin-shadcn-ui": "workspace:*",
68
68
  "esbuild": "0.28.1",
69
69
  "playwright-core": "^1.55.0",