@oceanum/oceanumlab 4.7.1 → 4.8.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 (51) hide show
  1. package/lib/DatameshWidget.d.ts +11 -0
  2. package/lib/DatameshWidget.js +35 -5
  3. package/lib/StoredNotebooks.d.ts +27 -5
  4. package/lib/StoredNotebooks.js +141 -24
  5. package/lib/auth/serverAuth.js +1 -0
  6. package/lib/auth/startSignIn.d.ts +11 -0
  7. package/lib/auth/startSignIn.js +16 -0
  8. package/lib/auth/tokens.d.ts +13 -0
  9. package/lib/index.d.ts +1 -1
  10. package/lib/index.js +43 -7
  11. package/lib/share/client.d.ts +86 -0
  12. package/lib/share/client.js +206 -0
  13. package/lib/share/events.d.ts +7 -0
  14. package/lib/share/events.js +9 -0
  15. package/lib/share/notebook.d.ts +216 -0
  16. package/lib/share/notebook.js +355 -0
  17. package/lib/share/oceanumOnly.d.ts +72 -0
  18. package/lib/share/oceanumOnly.js +127 -0
  19. package/lib/share/plugin.d.ts +23 -0
  20. package/lib/share/plugin.js +981 -0
  21. package/lib/share/upload.d.ts +6 -0
  22. package/lib/share/upload.js +27 -0
  23. package/lib/share/widgets.d.ts +25 -0
  24. package/lib/share/widgets.js +154 -0
  25. package/package.json +5 -1
  26. package/schema/datamesh-connect.json +6 -0
  27. package/src/DatameshWidget.tsx +79 -2
  28. package/src/StoredNotebooks.tsx +274 -24
  29. package/src/__tests__/OceanumPanel.spec.tsx +159 -5
  30. package/src/__tests__/StoredNotebooks.spec.tsx +528 -0
  31. package/src/__tests__/oceanumOnly.spec.ts +317 -0
  32. package/src/__tests__/settingsSchema.spec.ts +11 -1
  33. package/src/__tests__/shareClient.spec.ts +328 -0
  34. package/src/__tests__/shareNotebook.spec.ts +624 -0
  35. package/src/__tests__/sharePlugin.spec.ts +664 -0
  36. package/src/auth/serverAuth.ts +1 -0
  37. package/src/auth/startSignIn.ts +23 -0
  38. package/src/auth/tokens.ts +13 -0
  39. package/src/index.ts +43 -7
  40. package/src/share/client.ts +291 -0
  41. package/src/share/events.ts +10 -0
  42. package/src/share/notebook.ts +525 -0
  43. package/src/share/oceanumOnly.ts +177 -0
  44. package/src/share/plugin.ts +1220 -0
  45. package/src/share/upload.ts +27 -0
  46. package/src/share/widgets.ts +217 -0
  47. package/style/index.css +212 -18
  48. package/lib/specStore.d.ts +0 -37
  49. package/lib/specStore.js +0 -57
  50. package/src/__tests__/specStore.spec.ts +0 -126
  51. package/src/specStore.ts +0 -82
@@ -1,126 +0,0 @@
1
- import { ISpecSummary, listNotebooks, partitionNotebooks } from '../specStore';
2
-
3
- function summary(overrides: Partial<ISpecSummary> = {}): ISpecSummary {
4
- return {
5
- id: 'nb-1',
6
- name: 'Wave analysis',
7
- description: null,
8
- modified: '2026-05-17T12:00:00',
9
- creator: null,
10
- ...overrides
11
- };
12
- }
13
-
14
- function response(body: unknown, status = 200): Response {
15
- return {
16
- ok: status >= 200 && status < 300,
17
- status,
18
- json: async () => body
19
- } as unknown as Response;
20
- }
21
-
22
- describe('listNotebooks', () => {
23
- it('asks the notebook spec type with the token as a bearer credential', async () => {
24
- const calls: [string, RequestInit | undefined][] = [];
25
- const fetcher = (async (url: string, init?: RequestInit) => {
26
- calls.push([url, init]);
27
- return response([]);
28
- }) as unknown as typeof fetch;
29
-
30
- await listNotebooks('https://specs.example.com', 'jwt', fetcher);
31
-
32
- expect(calls[0][0]).toBe('https://specs.example.com/specs/notebook');
33
- expect((calls[0][1]?.headers as Record<string, string>).Authorization).toBe(
34
- 'Bearer jwt'
35
- );
36
- });
37
-
38
- it('tolerates a trailing slash on the service url', async () => {
39
- const calls: string[] = [];
40
- const fetcher = (async (url: string) => {
41
- calls.push(url);
42
- return response([]);
43
- }) as unknown as typeof fetch;
44
-
45
- await listNotebooks('https://specs.example.com//', 'jwt', fetcher);
46
-
47
- expect(calls[0]).toBe('https://specs.example.com/specs/notebook');
48
- });
49
-
50
- it('reports the status rather than returning an empty list', async () => {
51
- // The store answers a bad or expired token with 400, so silently returning []
52
- // would look exactly like an account with no notebooks.
53
- const fetcher = (async () =>
54
- response({ detail: 'bad' }, 400)) as unknown as typeof fetch;
55
- await expect(
56
- listNotebooks('https://specs.example.com', 'jwt', fetcher)
57
- ).rejects.toThrow('HTTP 400');
58
- });
59
-
60
- it('rejects a body that is not a list', async () => {
61
- const fetcher = (async () =>
62
- response({ detail: 'nope' })) as unknown as typeof fetch;
63
- await expect(
64
- listNotebooks('https://specs.example.com', 'jwt', fetcher)
65
- ).rejects.toThrow('unexpected response');
66
- });
67
-
68
- it('drops entries that are not summaries rather than rendering undefined', async () => {
69
- const fetcher = (async () =>
70
- response([
71
- summary({ id: 'good' }),
72
- { id: 'bad' },
73
- null
74
- ])) as unknown as typeof fetch;
75
- const items = await listNotebooks(
76
- 'https://specs.example.com',
77
- 'jwt',
78
- fetcher
79
- );
80
- expect(items.map(item => item.id)).toEqual(['good']);
81
- });
82
- });
83
-
84
- describe('partitionNotebooks', () => {
85
- it('splits the caller own records from the rest', () => {
86
- const mine = summary({ id: 'mine', creator: 'dave@oceanum.science' });
87
- const theirs = summary({ id: 'theirs', creator: null });
88
- const { mine: own, shared } = partitionNotebooks(
89
- [mine, theirs],
90
- 'dave@oceanum.science'
91
- );
92
- expect(own.map(i => i.id)).toEqual(['mine']);
93
- expect(shared.map(i => i.id)).toEqual(['theirs']);
94
- });
95
-
96
- it('compares addresses case-insensitively', () => {
97
- // The store matches entities as case-sensitive globs, so the claim and the stored
98
- // creator can differ in case for the same person.
99
- const { mine } = partitionNotebooks(
100
- [summary({ creator: 'Dave@Oceanum.Science' })],
101
- 'dave@oceanum.science'
102
- );
103
- expect(mine).toHaveLength(1);
104
- });
105
-
106
- it('treats everything as shared when nobody is signed in', () => {
107
- const { mine, shared } = partitionNotebooks(
108
- [summary({ creator: null }), summary({ id: 'b', creator: 'a@b.com' })],
109
- null
110
- );
111
- expect(mine).toEqual([]);
112
- expect(shared).toHaveLength(2);
113
- });
114
-
115
- it('orders each group newest first without mutating the input', () => {
116
- const input = [
117
- summary({ id: 'old', modified: '2026-01-01T00:00:00' }),
118
- summary({ id: 'new', modified: '2026-05-17T00:00:00' })
119
- ];
120
- expect(partitionNotebooks(input, null).shared.map(i => i.id)).toEqual([
121
- 'new',
122
- 'old'
123
- ]);
124
- expect(input.map(i => i.id)).toEqual(['old', 'new']);
125
- });
126
- });
package/src/specStore.ts DELETED
@@ -1,82 +0,0 @@
1
- /**
2
- * Reading the Oceanum spec store's `notebook` records.
3
- *
4
- * Deliberately small: only what the Stored notebooks tab needs. oceanum-notebook's
5
- * share-oceanum package has a fuller client (create, update, permissions), and when
6
- * sharing moves here the two should become one rather than sitting side by side.
7
- */
8
-
9
- /** A spec store list entry. The list omits the notebook body. */
10
- export interface ISpecSummary {
11
- id: string;
12
- name: string;
13
- description: string | null;
14
- /** Set when the record is created and never updated, so this is a creation time. */
15
- modified: string;
16
- /** Null unless the caller created the record: the store hides other creators. */
17
- creator: string | null;
18
- }
19
-
20
- function isSummary(value: unknown): value is ISpecSummary {
21
- if (typeof value !== 'object' || value === null) {
22
- return false;
23
- }
24
- const v = value as Record<string, unknown>;
25
- return (
26
- typeof v.id === 'string' &&
27
- typeof v.name === 'string' &&
28
- typeof v.modified === 'string'
29
- );
30
- }
31
-
32
- /**
33
- * Every notebook the signed-in user may read: their own and those shared with them.
34
- *
35
- * The store answers an unauthenticated caller with an empty list rather than an error,
36
- * so a missing token is indistinguishable from an empty account — callers should check
37
- * they are signed in before showing "no notebooks".
38
- */
39
- export async function listNotebooks(
40
- specsUrl: string,
41
- token: string,
42
- fetcher: typeof fetch = fetch
43
- ): Promise<ISpecSummary[]> {
44
- const url = `${specsUrl.replace(/\/+$/, '')}/specs/notebook`;
45
- const response = await fetcher(url, {
46
- headers: { Authorization: `Bearer ${token}` }
47
- });
48
- if (!response.ok) {
49
- // The store answers a bad or expired token with 400, and 403 both for no access
50
- // and for a record that does not exist.
51
- throw new Error(`Oceanum.io returned HTTP ${response.status}.`);
52
- }
53
- const data = await response.json();
54
- if (!Array.isArray(data)) {
55
- throw new Error('Oceanum.io returned an unexpected response.');
56
- }
57
- return data.filter(isSummary);
58
- }
59
-
60
- /**
61
- * Split a listing into the caller's own notebooks and the rest, newest first.
62
- *
63
- * The store only sends `creator` on records the caller created, so anything else it
64
- * returns is shared with them — directly, or because it is public. Addresses are
65
- * compared case-insensitively: the store matches entities as case-sensitive globs, so
66
- * the two sides can differ in case.
67
- */
68
- export function partitionNotebooks(
69
- items: readonly ISpecSummary[],
70
- email: string | null
71
- ): { mine: ISpecSummary[]; shared: ISpecSummary[] } {
72
- const me = email?.toLowerCase() ?? null;
73
- const sorted = [...items].sort((a, b) =>
74
- b.modified.localeCompare(a.modified)
75
- );
76
- const isMine = (item: ISpecSummary): boolean =>
77
- me !== null && item.creator?.toLowerCase() === me;
78
- return {
79
- mine: sorted.filter(isMine),
80
- shared: sorted.filter(item => !isMine(item))
81
- };
82
- }