@palantir/pack.state.foundry 0.1.2 → 0.2.1

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.
@@ -16,24 +16,38 @@
16
16
 
17
17
  import type {
18
18
  CreateDocumentRequest,
19
- DocumentSecurity,
19
+ DiscretionaryPrincipal,
20
+ DiscretionaryPrincipal as WireDiscretionaryPrincipal,
21
+ DocumentSecurity as WireDocumentSecurity,
20
22
  SearchDocumentsRequest,
21
23
  } from "@osdk/foundry.pack";
22
24
  import { Documents } from "@osdk/foundry.pack";
23
- import type { ModuleConfigTuple, PackAppInternal, Unsubscribe } from "@palantir/pack.core";
25
+ import {
26
+ assertNever,
27
+ getOntologyRid,
28
+ type ModuleConfigTuple,
29
+ type PackAppInternal,
30
+ type Unsubscribe,
31
+ } from "@palantir/pack.core";
24
32
  import type {
25
33
  ActivityEvent,
26
34
  DocumentId,
27
35
  DocumentMetadata,
28
36
  DocumentRef,
29
37
  DocumentSchema,
38
+ DocumentSecurity,
30
39
  Model,
31
40
  ModelData,
32
41
  PresenceEvent,
33
42
  PresenceSubscriptionOptions,
34
43
  } from "@palantir/pack.document-schema.model-types";
35
44
  import { getMetadata } from "@palantir/pack.document-schema.model-types";
36
- import type { DocumentService, InternalYjsDoc } from "@palantir/pack.state.core";
45
+ import type {
46
+ CreateDocumentMetadata,
47
+ DocumentService,
48
+ InternalYjsDoc,
49
+ SearchDocumentsResult,
50
+ } from "@palantir/pack.state.core";
37
51
  import {
38
52
  BaseYjsDocumentService,
39
53
  createDocumentServiceConfig,
@@ -44,6 +58,10 @@ import { createFoundryEventService } from "@palantir/pack.state.foundry-event";
44
58
  import { getActivityEvent, getPresenceEvent } from "./eventMappers.js";
45
59
 
46
60
  const DEFAULT_USE_PREVIEW_API = true;
61
+ const EMPTY_DOCUMENT_SECURITY: DocumentSecurity = Object.freeze({
62
+ discretionary: {},
63
+ mandatory: {},
64
+ });
47
65
 
48
66
  interface FoundryDocumentServiceConfig {
49
67
  readonly usePreviewApi?: boolean;
@@ -83,7 +101,10 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
83
101
  ) {
84
102
  super(
85
103
  app,
86
- app.config.logger.child({}, { level: "debug", msgPrefix: "FoundryDocumentService" }),
104
+ app.config.logger.child(
105
+ {},
106
+ { level: "debug", msgPrefix: "FoundryDocumentService" },
107
+ ),
87
108
  );
88
109
  }
89
110
 
@@ -98,22 +119,23 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
98
119
  }
99
120
 
100
121
  get hasMetadataSubscriptions(): boolean {
101
- return Array.from(this.documents.values()).some(doc =>
102
- this.hasSubscriptions(doc) && doc.metadataSubscribers.size > 0
122
+ return Array.from(this.documents.values()).some(
123
+ doc => this.hasSubscriptions(doc) && doc.metadataSubscribers.size > 0,
103
124
  );
104
125
  }
105
126
 
106
127
  get hasStateSubscriptions(): boolean {
107
- return Array.from(this.documents.values()).some(doc =>
108
- this.hasSubscriptions(doc) && doc.docStateSubscribers.size > 0
128
+ return Array.from(this.documents.values()).some(
129
+ doc => this.hasSubscriptions(doc) && doc.docStateSubscribers.size > 0,
109
130
  );
110
131
  }
111
132
 
112
133
  readonly createDocument = async <T extends DocumentSchema>(
113
- metadata: DocumentMetadata,
134
+ metadata: CreateDocumentMetadata,
114
135
  schema: T,
115
136
  ): Promise<DocumentRef<T>> => {
116
- const { documentTypeName, name, ontologyRid, security } = metadata;
137
+ const { documentTypeName, name, security } = metadata;
138
+ const ontologyRid = await getOntologyRid(this.app);
117
139
 
118
140
  const request: CreateDocumentRequest = {
119
141
  documentTypeName: documentTypeName,
@@ -121,9 +143,13 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
121
143
  ontologyRid: ontologyRid,
122
144
  security: getWireSecurity(security),
123
145
  };
124
- const createResponse = await Documents.create(this.app.config.osdkClient, request, {
125
- preview: this.config.usePreviewApi ?? DEFAULT_USE_PREVIEW_API,
126
- });
146
+ const createResponse = await Documents.create(
147
+ this.app.config.osdkClient,
148
+ request,
149
+ {
150
+ preview: this.config.usePreviewApi ?? DEFAULT_USE_PREVIEW_API,
151
+ },
152
+ );
127
153
 
128
154
  const documentId = createResponse.id as DocumentId;
129
155
  const docRef = this.createDocRef(documentId, schema);
@@ -135,26 +161,43 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
135
161
  schema: T,
136
162
  options?: {
137
163
  documentName?: string;
138
- limit?: number;
164
+ pageSize?: number;
165
+ pageToken?: string;
139
166
  },
140
- ): Promise<ReadonlyArray<DocumentMetadata & { readonly id: DocumentId }>> => {
167
+ ): Promise<SearchDocumentsResult> => {
141
168
  const request: SearchDocumentsRequest = {
142
169
  documentTypeName,
143
- limit: options?.limit,
144
- searchQuery: options?.documentName ? { documentName: options.documentName } : undefined,
170
+ searchQuery: options != null
171
+ ? {
172
+ documentName: options.documentName,
173
+ pageSize: options.pageSize,
174
+ pageToken: options.pageToken,
175
+ }
176
+ : undefined,
145
177
  };
146
178
 
147
- const searchResponse = await Documents.search(this.app.config.osdkClient, request, {
148
- preview: this.config.usePreviewApi ?? DEFAULT_USE_PREVIEW_API,
149
- });
179
+ const searchResponse = await Documents.search(
180
+ this.app.config.osdkClient,
181
+ request,
182
+ {
183
+ preview: this.config.usePreviewApi ?? DEFAULT_USE_PREVIEW_API,
184
+ },
185
+ );
150
186
 
151
- return searchResponse.map(doc => ({
152
- documentTypeName: doc.documentTypeName,
153
- id: doc.id as DocumentId,
154
- name: doc.name,
155
- ontologyRid: "unknown",
156
- security: { discretionary: { owners: [] }, mandatory: {} },
157
- }));
187
+ return {
188
+ data: searchResponse.data.map(doc => ({
189
+ createdBy: doc.createdBy,
190
+ createdTime: doc.createdTime,
191
+ documentTypeName: doc.documentTypeName,
192
+ id: doc.id as DocumentId,
193
+ name: doc.name,
194
+ ontologyRid: "unknown",
195
+ security: { discretionary: { owners: [] }, mandatory: {} },
196
+ updatedBy: doc.updatedBy,
197
+ updatedTime: doc.updatedTime,
198
+ })),
199
+ nextPageToken: searchResponse.nextPageToken,
200
+ };
158
201
  };
159
202
 
160
203
  protected onMetadataSubscriptionOpened(
@@ -189,7 +232,9 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
189
232
  });
190
233
  })
191
234
  .catch((e: unknown) => {
192
- const error = new Error("Failed to load document metadata", { cause: e });
235
+ const error = new Error("Failed to load document metadata", {
236
+ cause: e,
237
+ });
193
238
  this.updateMetadataStatus(internalDoc, docRef, {
194
239
  error,
195
240
  load: DocumentLoadStatus.ERROR,
@@ -217,8 +262,7 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
217
262
  protected onMetadataSubscriptionClosed(
218
263
  _internalDoc: FoundryInternalDoc,
219
264
  _docRef: DocumentRef,
220
- ): void {
221
- }
265
+ ): void {}
222
266
 
223
267
  protected onDataSubscriptionClosed(
224
268
  internalDoc: FoundryInternalDoc,
@@ -239,21 +283,20 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
239
283
  unsubscribed = true;
240
284
  };
241
285
 
242
- this.eventService.subscribeToActivityUpdates(
243
- docRef.id,
244
- foundryEvent => {
286
+ this.eventService
287
+ .subscribeToActivityUpdates(docRef.id, foundryEvent => {
245
288
  if (!unsubscribed) {
246
289
  const localEvent = getActivityEvent(docRef.schema, foundryEvent);
247
290
  if (localEvent != null) {
248
291
  callback(docRef, localEvent);
249
292
  }
250
293
  }
251
- },
252
- ).catch((e: unknown) => {
253
- this.logger.error("Failed to subscribe to activity updates", e, {
254
- docId: docRef.id,
294
+ })
295
+ .catch((e: unknown) => {
296
+ this.logger.error("Failed to subscribe to activity updates", e, {
297
+ docId: docRef.id,
298
+ });
255
299
  });
256
- });
257
300
 
258
301
  return unsubscribeFn;
259
302
  }
@@ -268,20 +311,22 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
268
311
  unsubscribed = true;
269
312
  };
270
313
 
271
- this.eventService.subscribeToPresenceUpdates(
272
- docRef.id,
273
- foundryUpdate => {
274
- if (!unsubscribed) {
275
- const localEvent = getPresenceEvent(docRef.schema, foundryUpdate);
276
- callback(docRef, localEvent);
277
- }
278
- },
279
- options,
280
- ).catch((e: unknown) => {
281
- this.logger.error("Failed to subscribe to presence updates", e, {
282
- docId: docRef.id,
314
+ this.eventService
315
+ .subscribeToPresenceUpdates(
316
+ docRef.id,
317
+ foundryUpdate => {
318
+ if (!unsubscribed) {
319
+ const localEvent = getPresenceEvent(docRef.schema, foundryUpdate);
320
+ callback(docRef, localEvent);
321
+ }
322
+ },
323
+ options,
324
+ )
325
+ .catch((e: unknown) => {
326
+ this.logger.error("Failed to subscribe to presence updates", e, {
327
+ docId: docRef.id,
328
+ });
283
329
  });
284
- });
285
330
 
286
331
  return unsubscribeFn;
287
332
  }
@@ -293,34 +338,46 @@ export class FoundryDocumentService extends BaseYjsDocumentService<FoundryIntern
293
338
  ): void {
294
339
  const eventType = getMetadata(model).name;
295
340
 
296
- void this.eventService.publishCustomPresence(
297
- docRef.id,
298
- eventType,
299
- eventData,
300
- ).catch((e: unknown) => {
301
- this.logger.error("Failed to publish custom presence", e, {
302
- docId: docRef.id,
341
+ void this.eventService
342
+ .publishCustomPresence(docRef.id, eventType, eventData)
343
+ .catch((e: unknown) => {
344
+ this.logger.error("Failed to publish custom presence", e, {
345
+ docId: docRef.id,
346
+ });
303
347
  });
304
- });
305
348
  }
306
349
  }
307
350
 
308
- function mutableArray<T>(array?: readonly T[]): T[] {
309
- return array == null ? [] : (array as T[]);
310
- }
351
+ function getWireSecurity({
352
+ discretionary,
353
+ mandatory,
354
+ }: DocumentSecurity = EMPTY_DOCUMENT_SECURITY): WireDocumentSecurity {
355
+ const { editors = [], owners = [], viewers = [] } = discretionary;
311
356
 
312
- function getWireSecurity(
313
- security: DocumentMetadata["security"],
314
- ): DocumentSecurity {
315
357
  return {
316
358
  discretionary: {
317
- editors: [...(security.discretionary.editors ?? [])],
318
- owners: [...security.discretionary.owners],
319
- viewers: [...(security.discretionary.viewers ?? [])],
359
+ editors: editors.map(getWirePrincipal),
360
+ owners: owners.map(getWirePrincipal),
361
+ viewers: viewers.map(getWirePrincipal),
320
362
  },
321
363
  mandatory: {
322
- classification: mutableArray(security.mandatory.classification),
323
- markings: mutableArray(security.mandatory.markings),
364
+ classification: mandatory.classification != null ? [...mandatory.classification] : [],
365
+ markings: mandatory.markings != null ? [...mandatory.markings] : [],
324
366
  },
325
367
  };
326
368
  }
369
+
370
+ function getWirePrincipal(
371
+ principal: DiscretionaryPrincipal,
372
+ ): WireDiscretionaryPrincipal {
373
+ switch (principal.type) {
374
+ case "all":
375
+ return { type: "all" };
376
+ case "groupId":
377
+ return { type: "groupId", groupId: principal.groupId };
378
+ case "userId":
379
+ return { type: "userId", userId: principal.userId };
380
+ default:
381
+ assertNever(principal);
382
+ }
383
+ }
@@ -17,11 +17,16 @@
17
17
  import type {
18
18
  ActivityCollaborativeUpdate,
19
19
  ActivityEvent as FoundryActivityEvent,
20
+ PresenceCollaborativeUpdate,
20
21
  } from "@osdk/foundry.pack";
21
22
  import { invalidUserRef } from "@palantir/pack.auth";
22
23
  import type {
23
24
  ActivityEvent,
24
25
  ActivityEventData,
26
+ ActivityEventDataDocumentCreate,
27
+ ActivityEventDataDocumentDescriptionUpdate,
28
+ ActivityEventDataDocumentRename,
29
+ ActivityEventDataDocumentSecurityUpdate,
25
30
  DocumentSchema,
26
31
  Model,
27
32
  ModelData,
@@ -35,7 +40,6 @@ import {
35
40
  ActivityEventDataType,
36
41
  PresenceEventDataType,
37
42
  } from "@palantir/pack.document-schema.model-types";
38
- import type { PresenceCollaborativeUpdate } from "@palantir/pack.state.foundry-event";
39
43
 
40
44
  export function getActivityEvent(
41
45
  documentSchema: DocumentSchema,
@@ -58,11 +62,54 @@ export function getActivityEvent(
58
62
  };
59
63
  }
60
64
 
65
+ /**
66
+ * Platform event type names as sent by backpack.
67
+ * Note: Only DOCUMENT_CREATE is currently emitted by the backend.
68
+ * Remaining types are included for future use.
69
+ */
70
+ const PlatformEventType = {
71
+ DOCUMENT_CREATE: "DocumentCreateEvent",
72
+ DOCUMENT_DESCRIPTION_UPDATE: "DocumentDescriptionUpdateEvent",
73
+ DOCUMENT_RENAME: "DocumentRenameEvent",
74
+ DOCUMENT_SECURITY_UPDATE: "DocumentMandatorySecurityUpdateEvent",
75
+ } as const;
76
+
77
+ interface WireDocumentCreateEvent {
78
+ readonly name?: string;
79
+ readonly initialMandatorySecurity?: {
80
+ readonly classification?: readonly string[];
81
+ readonly markings?: readonly string[];
82
+ };
83
+ }
84
+
85
+ interface WireDocumentRenameEvent {
86
+ readonly previousName?: string;
87
+ readonly newName?: string;
88
+ }
89
+
90
+ interface WireDocumentDescriptionUpdateEvent {
91
+ readonly newDescription?: string;
92
+ readonly isInitial?: boolean;
93
+ }
94
+
95
+ interface WireDocumentSecurityUpdateEvent {
96
+ readonly newClassification?: readonly string[];
97
+ readonly newMarkings?: readonly string[];
98
+ }
99
+
61
100
  function getActivityEventData(
62
101
  docSchema: DocumentSchema,
63
102
  { eventData, eventType }: FoundryActivityEvent,
64
103
  ): ActivityEventData {
65
- // TODO: handle standard activity events
104
+ const platformEventData = getPlatformActivityEventData(
105
+ eventType,
106
+ eventData.data,
107
+ );
108
+ if (platformEventData != null) {
109
+ return platformEventData;
110
+ }
111
+
112
+ // Handle custom application-defined activity events
66
113
  // TODO: validate model is valid for activity events
67
114
  const model = docSchema[eventType];
68
115
  if (model == null) {
@@ -82,6 +129,55 @@ function getActivityEventData(
82
129
  };
83
130
  }
84
131
 
132
+ function getPlatformActivityEventData(
133
+ eventType: string,
134
+ data: unknown,
135
+ ): ActivityEventData | undefined {
136
+ switch (eventType) {
137
+ case PlatformEventType.DOCUMENT_CREATE: {
138
+ const wireData = data as WireDocumentCreateEvent;
139
+ return {
140
+ initialMandatorySecurity: {
141
+ classification: wireData.initialMandatorySecurity?.classification,
142
+ markings: wireData.initialMandatorySecurity?.markings,
143
+ },
144
+ name: wireData.name ?? "",
145
+ type: ActivityEventDataType.DOCUMENT_CREATE,
146
+ } satisfies ActivityEventDataDocumentCreate;
147
+ }
148
+
149
+ case PlatformEventType.DOCUMENT_RENAME: {
150
+ const wireData = data as WireDocumentRenameEvent;
151
+ return {
152
+ newName: wireData.newName ?? "",
153
+ previousName: wireData.previousName ?? "",
154
+ type: ActivityEventDataType.DOCUMENT_RENAME,
155
+ } satisfies ActivityEventDataDocumentRename;
156
+ }
157
+
158
+ case PlatformEventType.DOCUMENT_DESCRIPTION_UPDATE: {
159
+ const wireData = data as WireDocumentDescriptionUpdateEvent;
160
+ return {
161
+ isInitial: wireData.isInitial ?? false,
162
+ newDescription: wireData.newDescription ?? "",
163
+ type: ActivityEventDataType.DOCUMENT_DESCRIPTION_UPDATE,
164
+ } satisfies ActivityEventDataDocumentDescriptionUpdate;
165
+ }
166
+
167
+ case PlatformEventType.DOCUMENT_SECURITY_UPDATE: {
168
+ const wireData = data as WireDocumentSecurityUpdateEvent;
169
+ return {
170
+ newClassification: wireData.newClassification ?? [],
171
+ newMarkings: wireData.newMarkings ?? [],
172
+ type: ActivityEventDataType.DOCUMENT_SECURITY_UPDATE,
173
+ } satisfies ActivityEventDataDocumentSecurityUpdate;
174
+ }
175
+
176
+ default:
177
+ return undefined;
178
+ }
179
+ }
180
+
85
181
  const ARRIVED_DATA: PresenceEventDataArrived = {
86
182
  type: PresenceEventDataType.ARRIVED,
87
183
  } as const;
@@ -96,7 +192,7 @@ export function getPresenceEvent(
96
192
  ): PresenceEvent {
97
193
  switch (foundryUpdate.type) {
98
194
  case "presenceChangeEvent": {
99
- const { userId, status } = foundryUpdate.presenceChangeEvent;
195
+ const { userId, status } = foundryUpdate;
100
196
  const eventData = status === "PRESENT" ? ARRIVED_DATA : DEPARTED_DATA;
101
197
  return {
102
198
  eventData,
@@ -105,7 +201,7 @@ export function getPresenceEvent(
105
201
  }
106
202
 
107
203
  case "customPresenceEvent": {
108
- const { userId, eventData } = foundryUpdate.customPresenceEvent;
204
+ const { userId, eventData } = foundryUpdate;
109
205
  const presenceEventData = getPresenceEventData(documentSchema, eventData);
110
206
  return {
111
207
  eventData: presenceEventData,