@xpert-ai/plugin-spaces 0.0.0-stage → 0.2.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.
Files changed (61) hide show
  1. package/README.md +25 -2
  2. package/dist/access.d.ts +38 -0
  3. package/dist/access.js +96 -0
  4. package/dist/ai.d.ts +43 -0
  5. package/dist/ai.js +143 -0
  6. package/dist/artifacts.d.ts +9 -0
  7. package/dist/artifacts.js +40 -0
  8. package/dist/capability.d.ts +22 -0
  9. package/dist/capability.js +63 -0
  10. package/dist/content.d.ts +19 -0
  11. package/dist/content.js +47 -0
  12. package/dist/documents.d.ts +36 -0
  13. package/dist/documents.js +158 -0
  14. package/dist/domain.d.ts +95 -0
  15. package/dist/domain.js +23 -0
  16. package/dist/editors.d.ts +16 -0
  17. package/dist/editors.js +63 -0
  18. package/dist/entities.d.ts +68 -0
  19. package/dist/entities.js +251 -0
  20. package/dist/files.d.ts +79 -0
  21. package/dist/files.js +228 -0
  22. package/dist/imports.d.ts +83 -0
  23. package/dist/imports.js +139 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +48 -0
  26. package/dist/knowledge.d.ts +43 -0
  27. package/dist/knowledge.js +166 -0
  28. package/dist/middleware.d.ts +17 -0
  29. package/dist/middleware.js +82 -0
  30. package/dist/pages.d.ts +38 -0
  31. package/dist/pages.js +41 -0
  32. package/dist/proposal-id.d.ts +3 -0
  33. package/dist/proposal-id.js +11 -0
  34. package/dist/references.d.ts +22 -0
  35. package/dist/references.js +119 -0
  36. package/dist/remote/app.css +4 -0
  37. package/dist/remote/app.js +167 -0
  38. package/dist/resource-provider.d.ts +36 -0
  39. package/dist/resource-provider.js +144 -0
  40. package/dist/spaces.d.ts +95 -0
  41. package/dist/spaces.js +228 -0
  42. package/dist/subpages.d.ts +24 -0
  43. package/dist/subpages.js +118 -0
  44. package/dist/view.d.ts +97 -0
  45. package/dist/view.js +226 -0
  46. package/docs/.mintignore +7 -0
  47. package/docs/AGENTS.md +3 -0
  48. package/docs/LICENSE +21 -0
  49. package/docs/README.md +3 -0
  50. package/docs/architecture.mdx +49 -0
  51. package/docs/collaboration.mdx +50 -0
  52. package/docs/docs.json +39 -0
  53. package/docs/feishu-document-import.mdx +118 -0
  54. package/docs/index.mdx +12 -0
  55. package/docs/insert-menu-plan.mdx +74 -0
  56. package/docs/math-subpages.mdx +62 -0
  57. package/docs/page-resources-and-connectors.mdx +103 -0
  58. package/docs/quickstart.mdx +17 -0
  59. package/migrations/20261023-document-import-origin.sql +4 -0
  60. package/migrations/20261023-subpages.sql +6 -0
  61. package/package.json +79 -4
@@ -0,0 +1,36 @@
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 { SpaceAccess } from './access.js';
3
+ import { SpaceFiles } from './files.js';
4
+ import { SpaceDocuments } from './documents.js';
5
+ import { SpaceKnowledge } from './knowledge.js';
6
+ import { SpaceReferences } from './references.js';
7
+ import { SpaceSubpages } from './subpages.js';
8
+ export declare class SpacesResourceProvider implements IContentResourceProvider {
9
+ private readonly access;
10
+ private readonly documents;
11
+ private readonly knowledge;
12
+ private readonly subpages;
13
+ private readonly referenceService;
14
+ private readonly files;
15
+ readonly title = "Spaces";
16
+ constructor(access: SpaceAccess, documents: SpaceDocuments, knowledge: SpaceKnowledge, subpages: SpaceSubpages, referenceService: SpaceReferences, files: SpaceFiles);
17
+ readonly editor: ContentResourceEditor;
18
+ readonly references: ContentReferenceProvider;
19
+ readonly pages: ContentPageProvider;
20
+ readonly publication: ContentPublicationObserver;
21
+ readonly locations: ContentLocationProvider;
22
+ validateLocationChange(input: Parameters<NonNullable<ContentLocationProvider['validateLocationChange']>>[0]): Promise<void>;
23
+ onLocationPrepared(input: Parameters<NonNullable<ContentLocationProvider['onLocationPrepared']>>[0]): Promise<void>;
24
+ onLocationChanged(input: Parameters<NonNullable<ContentLocationProvider['onLocationChanged']>>[0]): Promise<void>;
25
+ onDocumentPublished(input: Parameters<NonNullable<ContentPublicationObserver['onDocumentPublished']>>[0]): Promise<void>;
26
+ authorize(input: ContentResourceRequest & {
27
+ principal: ContentPrincipal;
28
+ }): Promise<{
29
+ providerKey: string;
30
+ resourceId: string;
31
+ title: string;
32
+ role: "editor" | "manager" | "owner" | "member";
33
+ canWrite: boolean;
34
+ canManage: boolean;
35
+ }>;
36
+ }
@@ -0,0 +1,144 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { ConflictException, Injectable } from '@nestjs/common';
11
+ import { ContentResourceProvider } from '@xpert-ai/plugin-sdk';
12
+ import { Space, SpaceItem } from './entities.js';
13
+ import { SpaceAccess } from './access.js';
14
+ import { SpaceFiles } from './files.js';
15
+ import { SpaceDocuments } from './documents.js';
16
+ import { SpaceKnowledge } from './knowledge.js';
17
+ import { denied, scoped } from './domain.js';
18
+ import { SpaceReferences } from './references.js';
19
+ import { SpaceSubpages } from './subpages.js';
20
+ let SpacesResourceProvider = class SpacesResourceProvider {
21
+ access;
22
+ documents;
23
+ knowledge;
24
+ subpages;
25
+ referenceService;
26
+ files;
27
+ title = 'Spaces';
28
+ constructor(access, documents, knowledge, subpages, referenceService, files) {
29
+ this.access = access;
30
+ this.documents = documents;
31
+ this.knowledge = knowledge;
32
+ this.subpages = subpages;
33
+ this.referenceService = referenceService;
34
+ this.files = files;
35
+ }
36
+ editor = {
37
+ describe: async (input) => {
38
+ const actor = { ...input.actor, projectId: input.projectId, assistantId: input.assistantId };
39
+ const { item, space } = await this.access.resource(actor, input.sourceResourceId, 'read');
40
+ if (!space.projectId || !item?.editorKey || !item.filePath || item.deleted)
41
+ throw denied();
42
+ return { editorKey: item.editorKey, projectId: space.projectId, path: item.filePath, title: item.title };
43
+ },
44
+ files: {
45
+ read: (input) => this.files.editorRead({ ...input.actor, projectId: input.projectId, assistantId: input.assistantId }, input.sourceResourceId),
46
+ write: (input) => this.files.editorWrite({ ...input.actor, projectId: input.projectId, assistantId: input.assistantId }, input.sourceResourceId, input.expectedRevision, input.buffer)
47
+ }
48
+ };
49
+ references = {
50
+ listReferences: (input) => this.referenceService.listReferences(input),
51
+ uploadReference: (input) => this.referenceService.uploadReference(input),
52
+ resolveReference: (input) => this.referenceService.resolveReference(input)
53
+ };
54
+ pages = {
55
+ pageHierarchy: (input) => this.subpages.hierarchy(input),
56
+ createChildPage: (input) => this.subpages.createChild(input)
57
+ };
58
+ publication = { onDocumentPublished: (input) => this.onDocumentPublished(input) };
59
+ locations = {
60
+ validateLocationChange: (input) => this.validateLocationChange(input),
61
+ onLocationPrepared: (input) => this.onLocationPrepared(input),
62
+ onLocationChanged: (input) => this.onLocationChanged(input)
63
+ };
64
+ async validateLocationChange(input) {
65
+ const actor = { ...input.actor, projectId: input.projectId, assistantId: input.assistantId };
66
+ const { item, space } = await this.access.resource(actor, input.source.resourceId, 'write', !input.deleted);
67
+ if (!item || space.projectId !== input.projectId || item.filePath !== input.path || item.version !== input.expectedSourceVersion)
68
+ throw new ConflictException('spaces.version_conflict');
69
+ if (item.deleted !== input.deleted)
70
+ await this.access.resource(actor, item.id, 'manage', !input.deleted);
71
+ const parent = await this.access.resource(actor, input.parentResourceId ?? item.spaceId, 'write');
72
+ if (parent.space.id !== item.spaceId || (parent.item && parent.item.kind !== 'folder'))
73
+ throw denied();
74
+ let cursor = parent.item;
75
+ while (cursor) {
76
+ if (cursor.id === item.id)
77
+ throw new ConflictException('spaces.tree_cycle');
78
+ cursor = cursor.parentId ? await this.access.items.findOneByOrFail({ ...scoped(actor), id: cursor.parentId }) : null;
79
+ }
80
+ }
81
+ 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' });
83
+ }
84
+ async onLocationChanged(input) {
85
+ // Projection of a durable authorized operation: it must still finish if the initiating user leaves.
86
+ await this.access.items.manager.transaction(async (manager) => {
87
+ const repo = manager.getRepository(SpaceItem);
88
+ const item = await repo.findOneByOrFail({ ...scoped(input.actor), id: input.source.resourceId });
89
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(input.actor), id: item.spaceId, projectId: input.projectId }, lock: { mode: 'pessimistic_write' } });
90
+ const current = await repo.findOneByOrFail({ ...scoped(input.actor), id: item.id });
91
+ if (current.version === input.expectedSourceVersion + 1 && current.filePath === input.destination && current.deleted === input.deleted)
92
+ return;
93
+ if (current.version !== input.expectedSourceVersion || current.filePath !== input.path)
94
+ throw new ConflictException('spaces.version_conflict');
95
+ const descendants = await repo.findBy({ ...scoped(input.actor), spaceId: current.spaceId });
96
+ for (const child of descendants) {
97
+ if (!child.filePath?.startsWith(`${input.path}/`))
98
+ continue;
99
+ child.filePath = input.destination + child.filePath.slice(input.path.length);
100
+ child.deleted = input.deleted;
101
+ child.version++;
102
+ await repo.save(child);
103
+ }
104
+ current.filePath = input.destination;
105
+ current.title = input.title;
106
+ current.parentId = input.parentResourceId;
107
+ current.deleted = input.deleted;
108
+ current.version++;
109
+ current.syncStatus = 'ready';
110
+ current.syncError = null;
111
+ await repo.save(current);
112
+ });
113
+ }
114
+ async onDocumentPublished(input) {
115
+ const actor = { ...input.actor, projectId: input.projectId };
116
+ const { item, space } = await this.access.resource(actor, input.sourceResourceId, 'write');
117
+ if (!item || item.deleted || space.projectId !== input.projectId || item.filePath !== input.path)
118
+ return;
119
+ const disk = await this.documents.read(actor, space, item);
120
+ if (!disk || disk.sha256 !== input.hash)
121
+ return;
122
+ await this.access.items.update(item.id, { fileHash: input.hash, projectedSequence: input.sequence,
123
+ markdown: disk.buffer.toString('utf8'), syncStatus: 'ready', syncError: null });
124
+ await this.knowledge.sync(actor, space, item);
125
+ const indexed = await this.access.items.findOneByOrFail({ ...scoped(actor), id: item.id });
126
+ if (indexed.indexStatus === 'failed')
127
+ throw new Error('spaces.index_retry_required');
128
+ }
129
+ async authorize(input) {
130
+ const resourceId = input.resourceId;
131
+ const result = await this.access.resolve(input.principal, resourceId, input.operation);
132
+ // Resource IDs identify items. File paths only constrain that item's Project location.
133
+ if (input.filePath && input.filePath !== result.item?.filePath)
134
+ throw denied();
135
+ return { providerKey: 'spaces', resourceId, title: result.item?.title ?? result.space.title,
136
+ role: result.role, canWrite: result.canWrite, canManage: result.canManage };
137
+ }
138
+ };
139
+ SpacesResourceProvider = __decorate([
140
+ Injectable(),
141
+ ContentResourceProvider('spaces'),
142
+ __metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceKnowledge, SpaceSubpages, SpaceReferences, SpaceFiles])
143
+ ], SpacesResourceProvider);
144
+ export { SpacesResourceProvider };
@@ -0,0 +1,95 @@
1
+ import type { SpaceActor } from './domain.js';
2
+ import { SpaceArtifacts } from './artifacts.js';
3
+ import { Repository } from 'typeorm';
4
+ import { z } from 'zod/v3';
5
+ import { Space, SpaceItem } from './entities.js';
6
+ import { SpaceAccess } from './access.js';
7
+ import { SpaceDocuments } from './documents.js';
8
+ import { SpaceKnowledge } from './knowledge.js';
9
+ export declare const createItemSchema: z.ZodObject<{
10
+ spaceId: z.ZodString;
11
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
12
+ title: z.ZodString;
13
+ kind: z.ZodEnum<["folder", "page", "file", "artifact"]>;
14
+ artifactId: z.ZodOptional<z.ZodString>;
15
+ artifactVersionId: z.ZodOptional<z.ZodString>;
16
+ content: z.ZodOptional<z.ZodString>;
17
+ }, "strict", z.ZodTypeAny, {
18
+ kind: "folder" | "page" | "file" | "artifact";
19
+ title: string;
20
+ spaceId: string;
21
+ parentId: string | null;
22
+ artifactId?: string | undefined;
23
+ artifactVersionId?: string | undefined;
24
+ content?: string | undefined;
25
+ }, {
26
+ kind: "folder" | "page" | "file" | "artifact";
27
+ title: string;
28
+ spaceId: string;
29
+ parentId?: string | null | undefined;
30
+ artifactId?: string | undefined;
31
+ artifactVersionId?: string | undefined;
32
+ content?: string | undefined;
33
+ }>;
34
+ export declare const editItemSchema: z.ZodObject<{
35
+ itemId: z.ZodString;
36
+ expectedVersion: z.ZodNumber;
37
+ title: z.ZodOptional<z.ZodString>;
38
+ parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
39
+ deleted: z.ZodOptional<z.ZodBoolean>;
40
+ }, "strict", z.ZodTypeAny, {
41
+ itemId: string;
42
+ expectedVersion: number;
43
+ title?: string | undefined;
44
+ deleted?: boolean | undefined;
45
+ parentId?: string | null | undefined;
46
+ }, {
47
+ itemId: string;
48
+ expectedVersion: number;
49
+ title?: string | undefined;
50
+ deleted?: boolean | undefined;
51
+ parentId?: string | null | undefined;
52
+ }>;
53
+ export declare class SpacesService {
54
+ private readonly access;
55
+ private readonly artifacts;
56
+ private readonly documents;
57
+ private readonly knowledge;
58
+ private readonly spaces;
59
+ constructor(access: SpaceAccess, artifacts: SpaceArtifacts, documents: SpaceDocuments, knowledge: SpaceKnowledge, spaces: Repository<Space>);
60
+ /** One extension record per authorized Project, shared by all connected Assistants. */
61
+ workspace(actor: SpaceActor): Promise<{
62
+ title: string;
63
+ projectId: string;
64
+ knowledgebaseId: string | null;
65
+ ownerId: string;
66
+ visibility: "personal" | "team";
67
+ version: number;
68
+ deleted: boolean;
69
+ id: string;
70
+ tenantId: string;
71
+ organizationId: string;
72
+ createdAt: Date;
73
+ updatedAt: Date;
74
+ }>;
75
+ list(actor: SpaceActor): Promise<{
76
+ root: Space | SpaceItem;
77
+ role: "editor" | "manager" | "owner" | "member";
78
+ canWrite: boolean;
79
+ canManage: boolean;
80
+ items: (SpaceItem & {
81
+ role: string;
82
+ })[];
83
+ }>;
84
+ tree(actor: SpaceActor, resourceId: string, trash?: boolean): Promise<{
85
+ root: Space | SpaceItem;
86
+ role: "editor" | "manager" | "owner" | "member";
87
+ canWrite: boolean;
88
+ canManage: boolean;
89
+ items: (SpaceItem & {
90
+ role: string;
91
+ })[];
92
+ }>;
93
+ createItem(actor: SpaceActor, input: z.infer<typeof createItemSchema>): Promise<SpaceItem>;
94
+ editItem(actor: SpaceActor, input: z.infer<typeof editItemSchema>): Promise<SpaceItem>;
95
+ }
package/dist/spaces.js ADDED
@@ -0,0 +1,228 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { SpaceArtifacts } from './artifacts.js';
14
+ import { ConflictException, Injectable } from '@nestjs/common';
15
+ import { InjectRepository } from '@nestjs/typeorm';
16
+ import { Repository } from 'typeorm';
17
+ import { randomUUID } from 'node:crypto';
18
+ import { z } from 'zod/v3';
19
+ import { Space, SpaceItem } from './entities.js';
20
+ import { SpaceAccess } from './access.js';
21
+ import { denied, id, kindSchema, scoped, title } from './domain.js';
22
+ import { isInternalSpacePath, SpaceDocuments } from './documents.js';
23
+ import { SpaceKnowledge } from './knowledge.js';
24
+ export const createItemSchema = z.object({ spaceId: id, parentId: id.nullable().default(null), title,
25
+ kind: kindSchema, artifactId: id.optional(), artifactVersionId: id.optional(), content: z.string().max(100000).optional() }).strict();
26
+ export const editItemSchema = z.object({ itemId: id, expectedVersion: z.number().int().positive(),
27
+ title: title.optional(), parentId: id.nullable().optional(), deleted: z.boolean().optional() }).strict();
28
+ let SpacesService = class SpacesService {
29
+ access;
30
+ artifacts;
31
+ documents;
32
+ knowledge;
33
+ spaces;
34
+ constructor(access, artifacts, documents, knowledge, spaces) {
35
+ this.access = access;
36
+ this.artifacts = artifacts;
37
+ this.documents = documents;
38
+ this.knowledge = knowledge;
39
+ this.spaces = spaces;
40
+ }
41
+ /** One extension record per authorized Project, shared by all connected Assistants. */
42
+ async workspace(actor) {
43
+ const project = await this.access.project(actor);
44
+ const where = { ...scoped(actor), projectId: project.projectId };
45
+ await this.spaces.createQueryBuilder().insert().values({ ...where,
46
+ title: project.name ?? 'Space', ownerId: actor.userId, visibility: 'team'
47
+ }).orIgnore().execute();
48
+ const space = await this.spaces.findOneByOrFail(where);
49
+ if (space.deleted)
50
+ throw denied();
51
+ return { ...space, title: project.name ?? space.title };
52
+ }
53
+ async list(actor) {
54
+ const space = await this.workspace(actor);
55
+ return this.tree(actor, space.id);
56
+ }
57
+ async tree(actor, resourceId, trash = false) {
58
+ const principal = await this.access.principal(actor);
59
+ const root = await this.access.resolve(principal, resourceId, trash ? 'manage' : 'read');
60
+ if (!trash)
61
+ await this.spaces.manager.transaction(async (manager) => {
62
+ 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);
64
+ });
65
+ const all = await this.access.items.find({ where: { ...scoped(actor), spaceId: root.space.id }, order: { createdAt: 'ASC' } });
66
+ const items = [];
67
+ const descendants = new Set([resourceId]);
68
+ // Traverse by identity, never by a path supplied by the client.
69
+ for (let round = 0; round <= all.length; round++) {
70
+ let changed = false;
71
+ for (const item of all)
72
+ if (!descendants.has(item.id) && (resourceId === root.space.id || (item.parentId && descendants.has(item.parentId)))) {
73
+ descendants.add(item.id);
74
+ changed = true;
75
+ }
76
+ if (!changed)
77
+ break;
78
+ }
79
+ for (const item of all) {
80
+ if (!descendants.has(item.id) || item.deleted !== trash ||
81
+ (!trash && item.filePath && isInternalSpacePath(item.filePath)))
82
+ continue;
83
+ try {
84
+ const result = await this.access.resolve(principal, item.id, 'read', trash);
85
+ items.push({ ...item, role: result.role, parentId: item.id === resourceId ? null : item.parentId });
86
+ }
87
+ catch (error) {
88
+ if (!(error instanceof Error) || error.message !== 'spaces.access_denied')
89
+ throw error;
90
+ }
91
+ }
92
+ return { root: root.item ?? root.space, role: root.role, canWrite: root.canWrite, canManage: root.canManage, items };
93
+ }
94
+ async createItem(actor, input) {
95
+ const access = await this.access.resource(actor, input.parentId ?? input.spaceId, 'write');
96
+ if (access.space.id !== input.spaceId || (access.item && access.item.kind !== 'folder'))
97
+ throw denied();
98
+ if (input.kind !== 'page' && input.content !== undefined)
99
+ throw denied();
100
+ const { content, ...fields } = input;
101
+ {
102
+ const item = await this.access.items.save(this.access.items.create({
103
+ ...scoped(actor), ...fields, id: randomUUID(), createdById: actor.userId
104
+ }));
105
+ try {
106
+ await this.documents.initialize(actor, access.space, item, input.kind === 'page' ? Buffer.from(content ?? '', 'utf8') : undefined);
107
+ }
108
+ catch (error) {
109
+ await this.access.items.update(item.id, { deleted: true });
110
+ if (error && typeof error === 'object' && 'code' in error && error.code === '23505')
111
+ throw new ConflictException('spaces.file_name_conflict');
112
+ throw error;
113
+ }
114
+ if (input.kind === 'page') {
115
+ try {
116
+ item.artifactId = await this.artifacts.initializePage(actor, item.id, item.title);
117
+ }
118
+ catch (error) {
119
+ await this.access.items.update(item.id, { deleted: true });
120
+ throw error;
121
+ }
122
+ }
123
+ return item;
124
+ }
125
+ }
126
+ async editItem(actor, input) {
127
+ const result = await this.access.resource(actor, input.itemId, input.deleted !== undefined ? 'manage' : 'write', input.deleted === false);
128
+ if (!result.item)
129
+ throw denied();
130
+ if (result.space.projectId && result.item.filePath) {
131
+ const source = result.item;
132
+ const sourcePath = result.item.filePath;
133
+ const descendants = await this.access.items.findBy({ ...scoped(actor), spaceId: result.space.id });
134
+ if (source.editorKey === 'markdown.tiptap' || descendants.some((item) => item.editorKey === 'markdown.tiptap' && item.filePath?.startsWith(`${source.filePath}/`))) {
135
+ const target = Object.assign(new SpaceItem(), source, { title: input.title ?? source.title,
136
+ parentId: input.parentId === undefined ? source.parentId : input.parentId, deleted: input.deleted ?? source.deleted });
137
+ const destination = input.deleted === true ? `.space-trash/${source.id}/${sourcePath.split('/').pop()}` : await this.documents.path(actor, target);
138
+ await this.documents.content().move({ actor, assistantId: actor.assistantId, projectId: result.space.projectId,
139
+ source: { providerKey: 'spaces', resourceId: source.id }, path: sourcePath, destination,
140
+ title: target.title, parentResourceId: target.parentId, deleted: target.deleted, expectedSourceVersion: input.expectedVersion });
141
+ return this.access.items.findOneByOrFail({ ...scoped(actor), id: source.id });
142
+ }
143
+ }
144
+ let moved;
145
+ let changed;
146
+ let updated;
147
+ try {
148
+ updated = await this.spaces.manager.transaction(async (manager) => {
149
+ changed = [];
150
+ // A single Space lock prevents concurrent moves from introducing a cycle.
151
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: result.space.id }, lock: { mode: 'pessimistic_write' } });
152
+ await this.access.resource(actor, input.itemId, input.deleted !== undefined ? 'manage' : 'write', input.deleted === false);
153
+ const repo = manager.getRepository(SpaceItem);
154
+ const item = await repo.findOneByOrFail({ ...scoped(actor), id: input.itemId });
155
+ const previousPath = item.filePath;
156
+ if (item.version !== input.expectedVersion)
157
+ throw new ConflictException('spaces.version_conflict');
158
+ if (input.parentId !== undefined) {
159
+ const parent = await this.access.resource(actor, input.parentId ?? item.spaceId, 'write');
160
+ if (parent.space.id !== item.spaceId || (parent.item && parent.item.kind !== 'folder'))
161
+ throw denied();
162
+ let cursor = parent.item;
163
+ while (cursor) {
164
+ if (cursor.id === item.id)
165
+ throw new ConflictException('spaces.tree_cycle');
166
+ cursor = cursor.parentId ? await repo.findOneByOrFail({ ...scoped(actor), id: cursor.parentId }) : null;
167
+ }
168
+ item.parentId = input.parentId;
169
+ }
170
+ if (input.title !== undefined)
171
+ item.title = input.title;
172
+ if (input.deleted !== undefined)
173
+ item.deleted = input.deleted;
174
+ if (result.space.projectId && previousPath) {
175
+ const targetPath = input.deleted === true ? `.space-trash/${item.id}/${previousPath.split('/').pop()}`
176
+ : await this.documents.path(actor, item);
177
+ if (targetPath !== previousPath) {
178
+ await this.documents.api().move({ ...this.documents.scope(actor, result.space), path: previousPath, destination: targetPath });
179
+ moved = { path: targetPath, destination: previousPath };
180
+ const children = await repo.findBy({ ...scoped(actor), spaceId: item.spaceId });
181
+ for (const child of children) {
182
+ if (!child.filePath?.startsWith(`${previousPath}/`))
183
+ continue;
184
+ child.filePath = `${targetPath}${child.filePath.slice(previousPath.length)}`;
185
+ if (input.deleted !== undefined)
186
+ child.deleted = input.deleted;
187
+ child.version++;
188
+ await repo.save(child);
189
+ changed.push(child);
190
+ }
191
+ item.filePath = targetPath;
192
+ }
193
+ }
194
+ item.version++;
195
+ return repo.save(item);
196
+ });
197
+ }
198
+ catch (error) {
199
+ if (moved) {
200
+ try {
201
+ await this.documents.api().move({ ...this.documents.scope(actor, result.space), ...moved });
202
+ }
203
+ catch {
204
+ await this.access.items.update(input.itemId, { syncStatus: 'failed', syncError: 'spaces.move_recovery_required' });
205
+ }
206
+ }
207
+ throw error;
208
+ }
209
+ for (const item of [updated, ...changed]) {
210
+ if (item.editorKey === 'markdown.tiptap' && result.space.projectId && item.filePath) {
211
+ const scope = { actor, projectId: result.space.projectId, assistantId: actor.assistantId, resourceId: item.id };
212
+ if (input.deleted !== undefined)
213
+ await this.documents.content().setDeleted({ ...scope, deleted: input.deleted });
214
+ if (!item.deleted)
215
+ await this.documents.content().relocate({ ...scope, path: item.filePath, title: item.title });
216
+ }
217
+ await this.knowledge.sync(actor, result.space, item);
218
+ }
219
+ return updated;
220
+ }
221
+ };
222
+ SpacesService = __decorate([
223
+ Injectable(),
224
+ __param(4, InjectRepository(Space)),
225
+ __metadata("design:paramtypes", [SpaceAccess, SpaceArtifacts, SpaceDocuments,
226
+ SpaceKnowledge, Repository])
227
+ ], SpacesService);
228
+ export { SpacesService };
@@ -0,0 +1,24 @@
1
+ import type { ContentSourceInput } from '@xpert-ai/plugin-sdk';
2
+ import type { ContentPageHierarchy } from '@xpert-ai/contracts';
3
+ import { SpaceAccess } from './access.js';
4
+ import { SpaceDocuments } from './documents.js';
5
+ import { SpaceArtifacts } from './artifacts.js';
6
+ /** Logical hierarchy is metadata, never an ACL or a disguised file folder.
7
+ * Each create owns one stable resource/path. Retry resumes publication instead of duplicating a child.
8
+ * Parent rename/move/trash does not mutate independent child files; a restored parent reconnects them.
9
+ */
10
+ export declare class SpaceSubpages {
11
+ private readonly access;
12
+ private readonly documents;
13
+ private readonly artifacts;
14
+ constructor(access: SpaceAccess, documents: SpaceDocuments, artifacts: SpaceArtifacts);
15
+ private actor;
16
+ private parent;
17
+ hierarchy(input: ContentSourceInput): Promise<ContentPageHierarchy>;
18
+ createChild(input: ContentSourceInput & {
19
+ title: string;
20
+ requestId: string;
21
+ }): Promise<{
22
+ documentId: string;
23
+ }>;
24
+ }
@@ -0,0 +1,118 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { ConflictException, Injectable } from '@nestjs/common';
11
+ import { z } from 'zod/v3';
12
+ import { SpaceAccess } from './access.js';
13
+ import { Space } from './entities.js';
14
+ import { SpaceDocuments, contentHash, safeName } from './documents.js';
15
+ import { SpaceArtifacts } from './artifacts.js';
16
+ import { denied, scoped } from './domain.js';
17
+ const create = z.object({ requestId: z.string().uuid(), title: z.string().trim().min(1).max(180) });
18
+ const missing = (error) => error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
19
+ const entry = (item) => ({ resourceId: item.id, title: item.title, path: item.filePath });
20
+ /** Logical hierarchy is metadata, never an ACL or a disguised file folder.
21
+ * Each create owns one stable resource/path. Retry resumes publication instead of duplicating a child.
22
+ * Parent rename/move/trash does not mutate independent child files; a restored parent reconnects them.
23
+ */
24
+ let SpaceSubpages = class SpaceSubpages {
25
+ access;
26
+ documents;
27
+ artifacts;
28
+ constructor(access, documents, artifacts) {
29
+ this.access = access;
30
+ this.documents = documents;
31
+ this.artifacts = artifacts;
32
+ }
33
+ actor(input) { return { ...input.actor, projectId: input.projectId, assistantId: input.assistantId }; }
34
+ async parent(input, write = false) {
35
+ const result = await this.access.resource(this.actor(input), input.sourceResourceId, write ? 'write' : 'read');
36
+ if (!result.item || result.item.kind !== 'page' || !result.item.filePath || result.space.projectId !== input.projectId)
37
+ throw denied();
38
+ return { ...result, item: result.item };
39
+ }
40
+ async hierarchy(input) {
41
+ const { item, space, canWrite } = await this.parent(input);
42
+ const actor = this.actor(input);
43
+ const visible = async (candidate) => {
44
+ if (!candidate || candidate.deleted || candidate.editorKey !== 'markdown.tiptap' || !candidate.filePath)
45
+ return null;
46
+ try {
47
+ await this.access.resource(actor, candidate.id, 'read');
48
+ return entry(candidate);
49
+ }
50
+ catch (error) {
51
+ if (error instanceof Error && error.message === 'spaces.access_denied')
52
+ return null;
53
+ throw error;
54
+ }
55
+ };
56
+ const parent = item.parentPageId ? await visible(await this.access.items.findOneBy({ ...scoped(actor), spaceId: space.id, id: item.parentPageId })) : null;
57
+ const rows = await this.access.items.find({ where: { ...scoped(actor), spaceId: space.id, parentPageId: item.id, deleted: false },
58
+ order: { createdAt: 'ASC' }, take: 101 });
59
+ const children = (await Promise.all(rows.slice(0, 100).map(visible))).filter((value) => value !== null);
60
+ return { supported: true, canCreate: canWrite, parent, children, hasMore: rows.length > 100 };
61
+ }
62
+ async createChild(input) {
63
+ const request = create.parse(input), actor = this.actor(input);
64
+ const { space } = await this.parent(input, true);
65
+ // Use the same Space lock as folder moves/discovery. Only the lock uses this transaction;
66
+ // publication is a retryable saga and its intent must survive process failure.
67
+ return this.access.items.manager.transaction(async (manager) => {
68
+ await manager.getRepository(Space).findOneOrFail({ where: { ...scoped(actor), id: space.id }, lock: { mode: 'pessimistic_write' } });
69
+ const { item: parent } = await this.parent(input, true);
70
+ let item = await this.access.items.findOneBy({ id: request.requestId });
71
+ if (item && (item.tenantId !== actor.tenantId || item.organizationId !== actor.organizationId || item.spaceId !== space.id ||
72
+ item.parentPageId !== parent.id || item.childRequestTitle !== request.title || item.createdById !== actor.userId || item.deleted))
73
+ throw new ConflictException('spaces.child_request_conflict');
74
+ if (!item) {
75
+ // New IDs cannot make cycles. Bound depth so corrupt legacy metadata cannot loop forever.
76
+ // Count the child itself as level one, then count its ancestors.
77
+ let cursor = parent, depth = 1;
78
+ while (cursor) {
79
+ if (++depth > 32 || cursor.id === request.requestId)
80
+ throw new ConflictException('spaces.page_depth');
81
+ cursor = cursor.parentPageId ? await this.access.items.findOneBy({ ...scoped(actor), spaceId: space.id, id: cursor.parentPageId }) : null;
82
+ }
83
+ const directory = parent.filePath.slice(0, parent.filePath.lastIndexOf('/') + 1);
84
+ // Resource suffix avoids collisions between equally named children and unrelated files.
85
+ const filePath = `${directory}${safeName(request.title).slice(0, 100)}--${request.requestId}.md`;
86
+ item = await this.access.items.save(this.access.items.create({ ...scoped(actor), id: request.requestId,
87
+ spaceId: space.id, parentId: parent.parentId, parentPageId: parent.id, childRequestTitle: request.title,
88
+ createdById: actor.userId, kind: 'page', title: request.title, filePath, syncStatus: 'pending' }));
89
+ }
90
+ if (item.editorKey === 'markdown.tiptap')
91
+ return { documentId: item.id };
92
+ const fileScope = { actor, projectId: input.projectId, path: item.filePath };
93
+ const markdown = `# ${request.title.replace(/[\\`*_{}\[\]()#+.!<>|$]/gu, '\\$&')}\n\n`;
94
+ const buffer = Buffer.from(markdown), hash = contentHash(buffer);
95
+ const disk = await this.documents.api().read(fileScope).catch((error) => { if (missing(error))
96
+ return null; throw error; });
97
+ if (disk && disk.sha256 !== (item.fileHash ?? hash))
98
+ throw new ConflictException('spaces.file_conflict');
99
+ if (!disk) {
100
+ if (item.fileHash)
101
+ throw new ConflictException('spaces.file_conflict');
102
+ await this.documents.api().write({ ...fileScope, buffer, expectedHash: null });
103
+ }
104
+ await this.access.items.update(item.id, { fileHash: disk?.sha256 ?? hash, syncStatus: 'ready' });
105
+ await this.documents.content().register({ actor, assistantId: input.assistantId, projectId: input.projectId,
106
+ resourceId: item.id, path: item.filePath, title: item.title, modelKey: 'markdown.tiptap', source: { providerKey: 'spaces', resourceId: item.id } });
107
+ if (!item.artifactId)
108
+ await this.artifacts.initializePage(actor, item.id, item.title);
109
+ await this.access.items.update(item.id, { editorKey: 'markdown.tiptap' });
110
+ return { documentId: item.id };
111
+ });
112
+ }
113
+ };
114
+ SpaceSubpages = __decorate([
115
+ Injectable(),
116
+ __metadata("design:paramtypes", [SpaceAccess, SpaceDocuments, SpaceArtifacts])
117
+ ], SpaceSubpages);
118
+ export { SpaceSubpages };