@concord-consortium/object-storage 1.0.0-pre.5 → 1.0.0-pre.6

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.
@@ -1,39 +1,20 @@
1
- import { DemoObjectStorageConfig, IObjectStorage, ObjectMetadata, ObjectData, StoredObject, ObjectWithId, MonitorCallback, DemonitorFunction, AddOptions } from './types';
1
+ import { DemoObjectStorageConfig, IObjectStorage, ObjectMetadata, ObjectData, StoredObject, MonitorCallback, DemonitorFunction, AddOptions, ObjectMetadataWithId } from './types';
2
2
  export declare class DemoObjectStorage implements IObjectStorage {
3
3
  private config;
4
4
  private objects;
5
5
  private monitors;
6
6
  constructor(config: DemoObjectStorageConfig);
7
- /**
8
- * Lists metadata documents for objects owned by the current user
9
- */
10
- listMine(): Promise<ObjectWithId[]>;
11
- /**
12
- * Lists metadata documents for objects linked to the current user
13
- */
14
- listLinked(): Promise<ObjectWithId[]>;
7
+ private getQuestionMetadata;
15
8
  /**
16
9
  * Lists metadata documents for objects associated with specific question IDs
17
10
  */
18
- list(questionIds: string[]): Promise<ObjectWithId[]>;
19
- /**
20
- * Monitors metadata documents for objects owned by the current user
21
- * Invokes callback at start and on any change
22
- * Returns a function to stop monitoring
23
- */
24
- monitorMine(callback: MonitorCallback): DemonitorFunction;
25
- /**
26
- * Monitors metadata documents for objects linked to the current user
27
- * Invokes callback at start and on any change
28
- * Returns a function to stop monitoring
29
- */
30
- monitorLinked(callback: MonitorCallback): DemonitorFunction;
11
+ list(questionId: string): Promise<ObjectMetadataWithId[]>;
31
12
  /**
32
13
  * Monitors metadata documents for objects associated with specific question IDs
33
14
  * Invokes callback at start and on any change
34
15
  * Returns a function to stop monitoring
35
16
  */
36
- monitor(questionIds: string[], callback: MonitorCallback): DemonitorFunction;
17
+ monitor(questionId: string, callback: MonitorCallback): DemonitorFunction;
37
18
  /**
38
19
  * Adds both metadata and data documents for a new object
39
20
  * Returns the generated object ID (nanoid) or the provided ID if specified in options
@@ -11,76 +11,33 @@ class DemoObjectStorage {
11
11
  this.objects = new Map();
12
12
  this.monitors = new Map();
13
13
  }
14
- /**
15
- * Lists metadata documents for objects owned by the current user
16
- */
17
- async listMine() {
18
- return Array.from(this.objects.values());
19
- }
20
- /**
21
- * Lists metadata documents for objects linked to the current user
22
- */
23
- async listLinked() {
24
- // not applicable in demo storage
25
- return [];
14
+ getQuestionMetadata() {
15
+ // In demo mode, just return all objects since there are no other questions
16
+ // that can use this storage instance
17
+ return Array.from(this.objects.entries()).map(([id, obj]) => {
18
+ return { id, metadata: obj.metadata };
19
+ });
26
20
  }
27
21
  /**
28
22
  * Lists metadata documents for objects associated with specific question IDs
29
23
  */
30
- async list(questionIds) {
31
- // In demo mode, just return all objects
32
- return Array.from(this.objects.values());
33
- }
34
- /**
35
- * Monitors metadata documents for objects owned by the current user
36
- * Invokes callback at start and on any change
37
- * Returns a function to stop monitoring
38
- */
39
- monitorMine(callback) {
40
- const key = 'mine';
41
- if (!this.monitors.has(key)) {
42
- this.monitors.set(key, []);
43
- }
44
- this.monitors.get(key).push(callback);
45
- // Invoke callback immediately with current state
46
- callback(Array.from(this.objects.values()));
47
- return () => {
48
- const callbacks = this.monitors.get(key);
49
- if (callbacks) {
50
- const index = callbacks.indexOf(callback);
51
- if (index > -1) {
52
- callbacks.splice(index, 1);
53
- }
54
- }
55
- };
56
- }
57
- /**
58
- * Monitors metadata documents for objects linked to the current user
59
- * Invokes callback at start and on any change
60
- * Returns a function to stop monitoring
61
- */
62
- monitorLinked(callback) {
63
- // not applicable in demo storage
64
- callback([]);
65
- return () => {
66
- // no-op
67
- };
24
+ async list(questionId) {
25
+ return this.getQuestionMetadata();
68
26
  }
69
27
  /**
70
28
  * Monitors metadata documents for objects associated with specific question IDs
71
29
  * Invokes callback at start and on any change
72
30
  * Returns a function to stop monitoring
73
31
  */
74
- monitor(questionIds, callback) {
75
- const key = `monitor-${questionIds.join(',')}`;
76
- if (!this.monitors.has(key)) {
77
- this.monitors.set(key, []);
32
+ monitor(questionId, callback) {
33
+ if (!this.monitors.has(questionId)) {
34
+ this.monitors.set(questionId, []);
78
35
  }
79
- this.monitors.get(key).push(callback);
36
+ this.monitors.get(questionId).push(callback);
80
37
  // Invoke callback immediately with current state
81
- callback(Array.from(this.objects.values()));
38
+ callback(this.getQuestionMetadata());
82
39
  return () => {
83
- const callbacks = this.monitors.get(key);
40
+ const callbacks = this.monitors.get(questionId);
84
41
  if (callbacks) {
85
42
  const index = callbacks.indexOf(callback);
86
43
  if (index > -1) {
@@ -95,12 +52,7 @@ class DemoObjectStorage {
95
52
  */
96
53
  async add(object, options) {
97
54
  const id = options?.id ?? (0, nanoid_1.nanoid)();
98
- const objectWithId = {
99
- id,
100
- metadata: object.metadata,
101
- data: object.data
102
- };
103
- this.objects.set(id, objectWithId);
55
+ this.objects.set(id, object);
104
56
  // Notify all monitors
105
57
  this.notifyMonitors();
106
58
  return id;
@@ -151,7 +103,7 @@ class DemoObjectStorage {
151
103
  * Notifies all active monitors of changes
152
104
  */
153
105
  notifyMonitors() {
154
- const allObjects = Array.from(this.objects.values());
106
+ const allObjects = this.getQuestionMetadata();
155
107
  this.monitors.forEach(callbacks => {
156
108
  callbacks.forEach(callback => callback(allObjects));
157
109
  });
@@ -1,6 +1,6 @@
1
1
  import "firebase/compat/auth";
2
2
  import "firebase/compat/firestore";
3
- import { FirebaseObjectStorageConfig, IObjectStorage, ObjectMetadata, ObjectData, StoredObject, ObjectWithId, MonitorCallback, DemonitorFunction, AddOptions } from './types';
3
+ import { FirebaseObjectStorageConfig, IObjectStorage, ObjectMetadata, ObjectData, StoredObject, MonitorCallback, DemonitorFunction, AddOptions, ObjectMetadataWithId } from './types';
4
4
  export declare class FirebaseObjectStorage implements IObjectStorage {
5
5
  private config;
6
6
  private initPromise;
@@ -12,36 +12,26 @@ export declare class FirebaseObjectStorage implements IObjectStorage {
12
12
  private createDocument;
13
13
  private getPaths;
14
14
  private getRefs;
15
+ private getMetadataQuery;
15
16
  /**
16
- * Lists metadata documents for objects owned by the current user
17
+ * Ensures the provided ID is in question ID format as opposed to reference ID format
18
+ *
19
+ * eg: "404-MWInteractive" transforms to "mw_interactive_404"
20
+ *
21
+ * @param questionOrRefId
22
+ * @returns
17
23
  */
18
- listMine(): Promise<ObjectWithId[]>;
19
- /**
20
- * Lists metadata documents for objects linked to the current user
21
- */
22
- listLinked(): Promise<ObjectWithId[]>;
24
+ private ensureIdIsQuestionId;
23
25
  /**
24
26
  * Lists metadata documents for objects associated with specific question IDs
25
27
  */
26
- list(questionIds: string[]): Promise<ObjectWithId[]>;
27
- /**
28
- * Monitors metadata documents for objects owned by the current user
29
- * Invokes callback at start and on any change
30
- * Returns a function to stop monitoring
31
- */
32
- monitorMine(callback: MonitorCallback): DemonitorFunction;
33
- /**
34
- * Monitors metadata documents for objects linked to the current user
35
- * Invokes callback at start and on any change
36
- * Returns a function to stop monitoring
37
- */
38
- monitorLinked(callback: MonitorCallback): DemonitorFunction;
28
+ list(questionOrRefId: string): Promise<ObjectMetadataWithId[]>;
39
29
  /**
40
30
  * Monitors metadata documents for objects associated with specific question IDs
41
31
  * Invokes callback at start and on any change
42
32
  * Returns a function to stop monitoring
43
33
  */
44
- monitor(questionIds: string[], callback: MonitorCallback): DemonitorFunction;
34
+ monitor(questionOrRefId: string, callback: MonitorCallback): DemonitorFunction;
45
35
  /**
46
36
  * Adds both metadata and data documents for a new object
47
37
  * Returns the generated object ID (nanoid) or the provided ID if specified in options
@@ -14,7 +14,9 @@ class FirebaseObjectStorage {
14
14
  if (config.version !== 1) {
15
15
  throw new Error(`Unsupported config version: ${config.version}. Expected version 1.`);
16
16
  }
17
- this.config = config;
17
+ this.config = { ...config };
18
+ // ensure the question id is in the correct format
19
+ this.config.questionId = this.ensureIdIsQuestionId(this.config.questionId);
18
20
  this.app = app_1.default.initializeApp(this.config.app);
19
21
  this.app.firestore().settings({
20
22
  ignoreUndefinedProperties: true,
@@ -36,15 +38,17 @@ class FirebaseObjectStorage {
36
38
  await this.initPromise;
37
39
  }
38
40
  createDocument(contents) {
41
+ const { questionId } = this.config;
39
42
  if (this.config.user.type === "authenticated") {
40
43
  const { contextId, platformId, platformUserId, resourceLinkId } = this.config.user;
41
44
  return {
42
45
  created_at: app_1.default.firestore.FieldValue.serverTimestamp(),
43
- platform_id: platformId,
44
- platform_user_id: platformUserId.toString(),
45
46
  context_id: contextId,
47
+ platform_id: platformId,
48
+ platform_user_id: platformUserId,
46
49
  resource_link_id: resourceLinkId,
47
50
  run_key: "",
51
+ question_id: questionId,
48
52
  ...contents
49
53
  };
50
54
  }
@@ -54,13 +58,14 @@ class FirebaseObjectStorage {
54
58
  created_at: app_1.default.firestore.FieldValue.serverTimestamp(),
55
59
  run_key: runKey,
56
60
  platform_user_id: runKey,
61
+ question_id: questionId,
57
62
  ...contents
58
63
  };
59
64
  }
60
65
  }
61
66
  getPaths(objectId) {
62
- const metadataPath = `${this.config.root}/object_store_metadata/${objectId}`;
63
- const dataPath = `${this.config.root}/object_store_data/${objectId}`;
67
+ const metadataPath = `${this.config.root}/object_store_metadata${objectId ? `/${objectId}` : ''}`;
68
+ const dataPath = `${this.config.root}/object_store_data${objectId ? `/${objectId}` : ''}`;
64
69
  return { metadataPath, dataPath };
65
70
  }
66
71
  getRefs(objectId) {
@@ -69,67 +74,73 @@ class FirebaseObjectStorage {
69
74
  const dataRef = this.app.firestore().doc(dataPath);
70
75
  return { metadataRef, dataRef };
71
76
  }
72
- /**
73
- * Lists metadata documents for objects owned by the current user
74
- */
75
- async listMine() {
76
- await this.ensureInitialized();
77
- // TODO: Implement Firebase query for user's own objects
78
- return [];
77
+ getMetadataQuery(questionOrRefId) {
78
+ const questionId = this.ensureIdIsQuestionId(questionOrRefId);
79
+ const { metadataPath } = this.getPaths();
80
+ let query = this.app.firestore().collection(metadataPath)
81
+ .where("question_id", "==", questionId);
82
+ if (this.config.user.type === "authenticated") { // logged in user
83
+ const { contextId, platformId, resourceLinkId, platformUserId } = this.config.user;
84
+ query = query
85
+ .where("context_id", "==", contextId)
86
+ .where("platform_id", "==", platformId)
87
+ .where("platform_user_id", "==", platformUserId.toString())
88
+ .where("resource_link_id", "==", resourceLinkId);
89
+ }
90
+ else {
91
+ query = query.where("run_key", "==", this.config.user.runKey);
92
+ }
93
+ query = query.orderBy("created_at", "asc");
94
+ return query;
79
95
  }
80
96
  /**
81
- * Lists metadata documents for objects linked to the current user
97
+ * Ensures the provided ID is in question ID format as opposed to reference ID format
98
+ *
99
+ * eg: "404-MWInteractive" transforms to "mw_interactive_404"
100
+ *
101
+ * @param questionOrRefId
102
+ * @returns
82
103
  */
83
- async listLinked() {
84
- await this.ensureInitialized();
85
- // TODO: Implement Firebase query for linked objects
86
- return [];
104
+ ensureIdIsQuestionId(questionOrRefId) {
105
+ const refIdRegEx = /(\d*)-(\D*)/g;
106
+ const parsed = refIdRegEx.exec(questionOrRefId);
107
+ if (parsed?.length) {
108
+ const [, embeddableId, embeddableType] = parsed;
109
+ const snakeCased = embeddableType.replace(/(?!^)([A-Z])/g, "_$1").toLowerCase();
110
+ return `${snakeCased}_${embeddableId}`;
111
+ }
112
+ return questionOrRefId;
87
113
  }
88
114
  /**
89
115
  * Lists metadata documents for objects associated with specific question IDs
90
116
  */
91
- async list(questionIds) {
117
+ async list(questionOrRefId) {
92
118
  await this.ensureInitialized();
93
- // TODO: Implement Firebase query for objects by question IDs
94
- return [];
95
- }
96
- /**
97
- * Monitors metadata documents for objects owned by the current user
98
- * Invokes callback at start and on any change
99
- * Returns a function to stop monitoring
100
- */
101
- monitorMine(callback) {
102
- // await this.ensureInitialized();
103
- // TODO: Implement Firebase realtime listener for user's own objects
104
- callback([]);
105
- return () => {
106
- // TODO: Implement cleanup
107
- };
108
- }
109
- /**
110
- * Monitors metadata documents for objects linked to the current user
111
- * Invokes callback at start and on any change
112
- * Returns a function to stop monitoring
113
- */
114
- monitorLinked(callback) {
115
- // await this.ensureInitialized();
116
- // TODO: Implement Firebase realtime listener for linked objects
117
- callback([]);
118
- return () => {
119
- // TODO: Implement cleanup
120
- };
119
+ const query = this.getMetadataQuery(questionOrRefId);
120
+ const querySnapshot = await query.get();
121
+ const results = [];
122
+ querySnapshot.forEach(doc => {
123
+ results.push({ id: doc.id, metadata: doc.data().metadata });
124
+ });
125
+ return results;
121
126
  }
122
127
  /**
123
128
  * Monitors metadata documents for objects associated with specific question IDs
124
129
  * Invokes callback at start and on any change
125
130
  * Returns a function to stop monitoring
126
131
  */
127
- monitor(questionIds, callback) {
132
+ monitor(questionOrRefId, callback) {
128
133
  // await this.ensureInitialized();
129
- // TODO: Implement Firebase realtime listener for objects by question IDs
130
- callback([]);
134
+ const query = this.getMetadataQuery(questionOrRefId);
135
+ const unsub = query.onSnapshot(snapshot => {
136
+ const results = [];
137
+ snapshot.forEach(doc => {
138
+ results.push({ id: doc.id, metadata: doc.data().metadata });
139
+ });
140
+ callback(results);
141
+ });
131
142
  return () => {
132
- // TODO: Implement cleanup
143
+ unsub();
133
144
  };
134
145
  }
135
146
  /**
@@ -154,28 +165,39 @@ class FirebaseObjectStorage {
154
165
  */
155
166
  async read(objectId) {
156
167
  await this.ensureInitialized();
157
- // TODO: Read metadata document from Firebase
158
- // TODO: Read data document from Firebase
159
- return {
160
- metadata: {},
161
- data: {}
162
- };
168
+ const metadata = await this.readMetadata(objectId);
169
+ if (!metadata) {
170
+ return undefined;
171
+ }
172
+ const data = await this.readData(objectId);
173
+ if (!data) {
174
+ return undefined;
175
+ }
176
+ return { metadata, data };
163
177
  }
164
178
  /**
165
179
  * Reads only the metadata document for an object
166
180
  */
167
181
  async readMetadata(objectId) {
168
182
  await this.ensureInitialized();
169
- // TODO: Read metadata document from Firebase
170
- return {};
183
+ const { metadataRef } = this.getRefs(objectId);
184
+ const metadataSnapshot = await metadataRef.get();
185
+ if (!metadataSnapshot.exists) {
186
+ return undefined;
187
+ }
188
+ return metadataSnapshot.data()?.metadata ?? {};
171
189
  }
172
190
  /**
173
191
  * Reads only the data document for an object
174
192
  */
175
193
  async readData(objectId) {
176
194
  await this.ensureInitialized();
177
- // TODO: Read data document from Firebase
178
- return {};
195
+ const { dataRef } = this.getRefs(objectId);
196
+ const dataSnapshot = await dataRef.get();
197
+ if (!dataSnapshot.exists) {
198
+ return undefined;
199
+ }
200
+ return dataSnapshot.data()?.data ?? {};
179
201
  }
180
202
  /**
181
203
  * Generates a new unique ID using nanoid
@@ -64,7 +64,7 @@ export declare class TypedObject implements StoredObject {
64
64
  data: TypedData;
65
65
  constructor(options?: TypedObjectOptions);
66
66
  static IsSupportedTypedObject(storedObject: StoredObject): boolean;
67
- static IsSupportedTypedObjectMetadata(storedObjectMetadata?: StoredObject["metadata"]): boolean;
67
+ static IsSupportedTypedObjectMetadata(storedObjectMetadata?: StoredObject["metadata"]): storedObjectMetadata is TypedMetadata;
68
68
  static FromStoredObject(id: string, storedObject: StoredObject): TypedObject;
69
69
  addImage(options: AddImageOptions): void;
70
70
  addDataTable(options: AddDataTableOptions): void;
package/dist/types.d.ts CHANGED
@@ -7,8 +7,8 @@ export interface AuthenticatedUser {
7
7
  jwt: string;
8
8
  contextId: string;
9
9
  platformId: string;
10
- platformUserId: string;
11
10
  resourceLinkId: string;
11
+ platformUserId: string;
12
12
  }
13
13
  export interface AnonymousUser {
14
14
  type: "anonymous";
@@ -18,21 +18,18 @@ export type FirebaseObjectStorageUser = AuthenticatedUser | AnonymousUser;
18
18
  export interface FirebaseObjectStorageConfig {
19
19
  version: 1;
20
20
  type: "firebase";
21
- app: any;
21
+ app: Object;
22
22
  root: string;
23
23
  user: FirebaseObjectStorageUser;
24
+ questionId: string;
24
25
  }
25
26
  export type ObjectStorageConfig = DemoObjectStorageConfig | FirebaseObjectStorageConfig;
26
27
  export interface AddOptions {
27
28
  id?: string;
28
29
  }
29
30
  export interface IObjectStorage {
30
- listMine(): Promise<ObjectWithId[]>;
31
- listLinked(): Promise<ObjectWithId[]>;
32
- list(questionIds: string[]): Promise<ObjectWithId[]>;
33
- monitorMine(callback: MonitorCallback): DemonitorFunction;
34
- monitorLinked(callback: MonitorCallback): DemonitorFunction;
35
- monitor(questionIds: string[], callback: MonitorCallback): DemonitorFunction;
31
+ list(questionOrRefId: string): Promise<ObjectMetadataWithId[]>;
32
+ monitor(questionOrRefId: string, callback: MonitorCallback): DemonitorFunction;
36
33
  add(object: StoredObject, options?: AddOptions): Promise<string>;
37
34
  read(objectId: string): Promise<StoredObject | undefined>;
38
35
  readMetadata(objectId: string): Promise<ObjectMetadata | undefined>;
@@ -49,8 +46,18 @@ export interface StoredObject {
49
46
  metadata: ObjectMetadata;
50
47
  data: ObjectData;
51
48
  }
52
- export interface ObjectWithId extends StoredObject {
49
+ export interface ObjectMetadataWithId {
50
+ id: string;
51
+ metadata: ObjectMetadata;
52
+ }
53
+ export interface ObjectDataWithId {
54
+ id: string;
55
+ data: ObjectData;
56
+ }
57
+ export interface ObjectWithId {
53
58
  id: string;
59
+ metadata: ObjectMetadata;
60
+ data: ObjectData;
54
61
  }
55
- export type MonitorCallback = (objects: ObjectWithId[]) => void;
62
+ export type MonitorCallback = (objects: ObjectMetadataWithId[]) => void;
56
63
  export type DemonitorFunction = () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@concord-consortium/object-storage",
3
- "version": "1.0.0-pre.5",
3
+ "version": "1.0.0-pre.6",
4
4
  "description": "A TypeScript library for object storage",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",