@oceanum/datamesh 0.2.0 → 0.4.2
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.
- package/README.md +6 -6
- package/package.json +6 -8
- package/src/lib/connector.ts +194 -37
- package/src/lib/datamodel.ts +322 -148
- package/src/lib/datasource.ts +35 -9
- package/src/lib/query.ts +2 -2
- package/src/lib/session.ts +171 -0
- package/src/lib/zarr.ts +93 -31
- package/src/test/dataframe.test.ts +1 -1
- package/src/test/dataset.test.ts +139 -19
- package/src/test/fixtures.ts +51 -48
- package/src/test/query.test.ts +3 -3
- package/tsconfig.vitest-temp.json +13 -2
- package/typedoc.json +1 -1
- package/vite.config.ts +1 -1
- package/dist/README.md +0 -31
- package/dist/blosc-CeItQ6qj.cjs +0 -17
- package/dist/blosc-DaK8KnI4.js +0 -719
- package/dist/browser-BDe_cnOJ.cjs +0 -1
- package/dist/browser-CJIXy_XB.js +0 -524
- package/dist/chunk-INHXZS53-DiyuLb3Z.js +0 -14
- package/dist/chunk-INHXZS53-z3BpFH8p.cjs +0 -1
- package/dist/gzip-DfmsOCZR.cjs +0 -1
- package/dist/gzip-TMN4LZ5e.js +0 -24
- package/dist/index.cjs +0 -9
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -11341
- package/dist/lib/connector.d.ts +0 -93
- package/dist/lib/connector.d.ts.map +0 -1
- package/dist/lib/datamodel.d.ts +0 -152
- package/dist/lib/datamodel.d.ts.map +0 -1
- package/dist/lib/datasource.d.ts +0 -96
- package/dist/lib/datasource.d.ts.map +0 -1
- package/dist/lib/observe.d.ts +0 -3
- package/dist/lib/observe.d.ts.map +0 -1
- package/dist/lib/query.d.ts +0 -135
- package/dist/lib/query.d.ts.map +0 -1
- package/dist/lib/zarr.d.ts +0 -20
- package/dist/lib/zarr.d.ts.map +0 -1
- package/dist/lz4-CssV0LoA.js +0 -643
- package/dist/lz4-PFaIsPAh.cjs +0 -15
- package/dist/test/fixtures.d.ts +0 -12
- package/dist/test/fixtures.d.ts.map +0 -1
- package/dist/zlib-C-RQJQaC.cjs +0 -1
- package/dist/zlib-DrihHfbK.js +0 -24
- package/dist/zstd-Cqadn9HA.js +0 -610
- package/dist/zstd-_xUhkGOV.cjs +0 -15
- package/src/docs/reverse_proxy.md +0 -0
- package/vite.config.ts.timestamp-1734584068599-c5119713c3c4e.mjs +0 -67
package/src/lib/datasource.ts
CHANGED
|
@@ -23,14 +23,40 @@ export type Coordinate =
|
|
|
23
23
|
| "j" // Coordinate_j
|
|
24
24
|
| "k"; // Coordinate_k
|
|
25
25
|
|
|
26
|
-
export type
|
|
26
|
+
export type Coordkeys = {
|
|
27
27
|
[key in Coordinate]?: string;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Represents the schema of a data source.
|
|
31
|
+
* Represents the internal schema of a data source.
|
|
32
32
|
*/
|
|
33
33
|
export type Schema = {
|
|
34
|
+
/**
|
|
35
|
+
* Attributes of the schema.
|
|
36
|
+
*/
|
|
37
|
+
attributes?: Record<string, string | number>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dimensions of the schema.
|
|
41
|
+
*/
|
|
42
|
+
dimensions: Record<string, number>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Coordinate map of the schema.
|
|
46
|
+
*/
|
|
47
|
+
coordkeys?: Coordkeys;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Data variables of the schema.
|
|
51
|
+
*/
|
|
52
|
+
variables: Record<string, DataVariable>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Datamesh schema
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
export type DatameshSchema = {
|
|
34
60
|
/**
|
|
35
61
|
* Attributes of the schema.
|
|
36
62
|
*/
|
|
@@ -42,14 +68,14 @@ export type Schema = {
|
|
|
42
68
|
dims: Record<string, number>;
|
|
43
69
|
|
|
44
70
|
/**
|
|
45
|
-
*
|
|
71
|
+
* Coordinate map of the schema.
|
|
46
72
|
*/
|
|
47
|
-
coords?: Record<string,
|
|
73
|
+
coords?: Record<string, DatameshSchema>;
|
|
48
74
|
|
|
49
75
|
/**
|
|
50
76
|
* Data variables of the schema.
|
|
51
77
|
*/
|
|
52
|
-
data_vars
|
|
78
|
+
data_vars?: Record<string, DatameshSchema>;
|
|
53
79
|
};
|
|
54
80
|
|
|
55
81
|
/**
|
|
@@ -74,7 +100,7 @@ export type Datasource = {
|
|
|
74
100
|
/**
|
|
75
101
|
* Parameters associated with the data source.
|
|
76
102
|
*/
|
|
77
|
-
parameters?: Record<string,
|
|
103
|
+
parameters?: Record<string, string | number>;
|
|
78
104
|
|
|
79
105
|
/**
|
|
80
106
|
* Geometric representation of the data source.
|
|
@@ -114,12 +140,12 @@ export type Datasource = {
|
|
|
114
140
|
/**
|
|
115
141
|
* Schema information for the data source.
|
|
116
142
|
*/
|
|
117
|
-
schema:
|
|
143
|
+
schema: DatameshSchema;
|
|
118
144
|
|
|
119
145
|
/**
|
|
120
|
-
* Coordinate
|
|
146
|
+
* Coordinate map for the data source.
|
|
121
147
|
*/
|
|
122
|
-
coordinates:
|
|
148
|
+
coordinates: Coordkeys;
|
|
123
149
|
|
|
124
150
|
/**
|
|
125
151
|
* Additional details about the data source.
|
package/src/lib/query.ts
CHANGED
|
@@ -129,7 +129,7 @@ export type CoordSelector = {
|
|
|
129
129
|
*/
|
|
130
130
|
export interface IQuery {
|
|
131
131
|
datasource: string;
|
|
132
|
-
parameters?: Record<string, number | string
|
|
132
|
+
parameters?: Record<string, number | string>;
|
|
133
133
|
description?: string;
|
|
134
134
|
variables?: string[];
|
|
135
135
|
timefilter?: TimeFilter;
|
|
@@ -163,7 +163,7 @@ export type Stage = {
|
|
|
163
163
|
*/
|
|
164
164
|
export class Query implements IQuery {
|
|
165
165
|
datasource: string;
|
|
166
|
-
parameters?: Record<string, number | string
|
|
166
|
+
parameters?: Record<string, number | string>;
|
|
167
167
|
description?: string;
|
|
168
168
|
variables?: string[];
|
|
169
169
|
timefilter?: TimeFilter;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { measureTime } from "./observe";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session class for datamesh connections.
|
|
5
|
+
*
|
|
6
|
+
* Sessions are used to manage authentication and resource allocation
|
|
7
|
+
* for datamesh operations.
|
|
8
|
+
*/
|
|
9
|
+
export class Session {
|
|
10
|
+
id!: string;
|
|
11
|
+
user!: string;
|
|
12
|
+
creationTime!: Date;
|
|
13
|
+
endTime!: Date;
|
|
14
|
+
write!: boolean;
|
|
15
|
+
verified: boolean = false;
|
|
16
|
+
private _connection!: any;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Acquire a session from the connection.
|
|
20
|
+
*
|
|
21
|
+
* @param connection - Connection object to acquire session from.
|
|
22
|
+
* @param options - Session options.
|
|
23
|
+
* @param options.duration - The desired length of time for the session in hours. Defaults to 1 hour.
|
|
24
|
+
* @returns A new session instance.
|
|
25
|
+
* @throws {Error} - If the session cannot be acquired.
|
|
26
|
+
*/
|
|
27
|
+
@measureTime
|
|
28
|
+
static async acquire(
|
|
29
|
+
connection: any,
|
|
30
|
+
options: { duration?: number } = {}
|
|
31
|
+
): Promise<Session> {
|
|
32
|
+
// Check if the connection supports sessions (v1 API)
|
|
33
|
+
if (!connection._isV1) {
|
|
34
|
+
const session = new Session();
|
|
35
|
+
session.id = "dummy_session";
|
|
36
|
+
session.user = "dummy_user";
|
|
37
|
+
session.creationTime = new Date();
|
|
38
|
+
session.endTime = new Date(
|
|
39
|
+
Date.now() + (options.duration || 1) * 60 * 60 * 1000
|
|
40
|
+
);
|
|
41
|
+
session.write = false;
|
|
42
|
+
session.verified = false;
|
|
43
|
+
session._connection = connection;
|
|
44
|
+
|
|
45
|
+
// Register cleanup function for when the process exits
|
|
46
|
+
if (typeof process !== "undefined" && process.on) {
|
|
47
|
+
process.on("beforeExit", () => {
|
|
48
|
+
session.close();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return session;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const headers = { ...connection._authHeaders };
|
|
57
|
+
headers["Cache-Control"] = "no-store";
|
|
58
|
+
const params = { duration: options.duration || 1 };
|
|
59
|
+
const response = await fetch(
|
|
60
|
+
`${connection._gateway}/session/?${params}`,
|
|
61
|
+
{ headers }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (response.status !== 200) {
|
|
65
|
+
throw new Error(`Failed to create session: ${await response.text()}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
const session = new Session();
|
|
70
|
+
session.id = data.id;
|
|
71
|
+
session.user = data.user;
|
|
72
|
+
session.creationTime = new Date(data.creation_time);
|
|
73
|
+
session.endTime = new Date(data.end_time);
|
|
74
|
+
session.write = data.write;
|
|
75
|
+
session.verified = data.verified || false;
|
|
76
|
+
session._connection = connection;
|
|
77
|
+
|
|
78
|
+
// Register cleanup function for when the process exits
|
|
79
|
+
if (typeof process !== "undefined" && process.on) {
|
|
80
|
+
process.on("beforeExit", () => {
|
|
81
|
+
session.close();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return session;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error(`Error when acquiring datamesh session: ${error}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the session header for requests.
|
|
93
|
+
*
|
|
94
|
+
* @returns The session header object.
|
|
95
|
+
*/
|
|
96
|
+
get header(): Record<string, string> {
|
|
97
|
+
return { "X-DATAMESH-SESSIONID": this.id };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Add session header to an existing headers object.
|
|
102
|
+
*
|
|
103
|
+
* @param headers - The headers object to add the session header to.
|
|
104
|
+
* @returns The updated headers object.
|
|
105
|
+
*/
|
|
106
|
+
addHeader(headers: Record<string, string>): Record<string, string> {
|
|
107
|
+
return { ...headers, ...this.header };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Close the session.
|
|
112
|
+
*
|
|
113
|
+
* @param finaliseWrite - Whether to finalise any write operations. Defaults to false.
|
|
114
|
+
* @throws {Error} - If the session cannot be closed and finaliseWrite is true.
|
|
115
|
+
*/
|
|
116
|
+
async close(finaliseWrite: boolean = false): Promise<void> {
|
|
117
|
+
// Back-compatibility with beta version (ignoring)
|
|
118
|
+
if (!this._connection._isV1) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Remove the beforeExit handler if possible
|
|
124
|
+
if (typeof process !== "undefined" && process.off) {
|
|
125
|
+
process.off("beforeExit", this.close);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const response = await fetch(
|
|
129
|
+
`${this._connection._gateway}/session/${this.id}`,
|
|
130
|
+
{
|
|
131
|
+
method: "DELETE",
|
|
132
|
+
headers: this.header,
|
|
133
|
+
body: JSON.stringify({ finalise_write: finaliseWrite }),
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (response.status !== 204) {
|
|
138
|
+
if (finaliseWrite) {
|
|
139
|
+
throw new Error(`Failed to finalise write: ${await response.text()}`);
|
|
140
|
+
}
|
|
141
|
+
console.warn(`Failed to close session: ${await response.text()}`);
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (finaliseWrite) {
|
|
145
|
+
throw new Error(`Error when closing datamesh session: ${error}`);
|
|
146
|
+
}
|
|
147
|
+
console.warn(`Error when closing datamesh session: ${error}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Enter a session context.
|
|
153
|
+
*
|
|
154
|
+
* @returns The session instance.
|
|
155
|
+
*/
|
|
156
|
+
async enter(): Promise<Session> {
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Exit a session context.
|
|
162
|
+
*
|
|
163
|
+
* @param error - Any error that occurred during the session.
|
|
164
|
+
* @returns A promise that resolves when the session is closed.
|
|
165
|
+
*/
|
|
166
|
+
async exit(error?: any): Promise<void> {
|
|
167
|
+
// When using context manager, close the session
|
|
168
|
+
// and finalise the write if no exception was raised
|
|
169
|
+
await this.close(error === undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/lib/zarr.ts
CHANGED
|
@@ -12,36 +12,73 @@ function delay(t: number): Promise<void> {
|
|
|
12
12
|
return new Promise((resolve) => setTimeout(resolve, t));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
interface CachedHTTPStoreOptions {
|
|
16
|
+
parameters?: Record<string, string | number>;
|
|
17
|
+
chunks?: string;
|
|
18
|
+
downsample?: Record<string, number>;
|
|
19
|
+
nocache?: boolean;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export class CachedHTTPStore implements AsyncReadable {
|
|
16
24
|
cache: UseStore | undefined;
|
|
17
25
|
url: string;
|
|
26
|
+
params: Record<string, string>;
|
|
18
27
|
cache_prefix: string;
|
|
19
28
|
fetchOptions: RequestInit;
|
|
29
|
+
timeout: number;
|
|
30
|
+
_pending: Record<string, boolean> = {};
|
|
31
|
+
|
|
20
32
|
constructor(
|
|
21
33
|
root: string,
|
|
22
34
|
authHeaders: Record<string, string>,
|
|
23
|
-
|
|
24
|
-
chunks?: string,
|
|
25
|
-
downsample?: Record<string, number>,
|
|
26
|
-
nocache?: boolean
|
|
35
|
+
options: CachedHTTPStoreOptions = {}
|
|
27
36
|
) {
|
|
37
|
+
// Create a copy of the auth headers to avoid modifying the original
|
|
38
|
+
// Important: We need to preserve all auth headers, including session headers
|
|
28
39
|
const headers = { ...authHeaders };
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
40
|
+
|
|
41
|
+
// Add parameters, chunks, and downsample as headers if provided
|
|
42
|
+
if (options.parameters)
|
|
43
|
+
headers["x-parameters"] = JSON.stringify(options.parameters);
|
|
44
|
+
if (options.chunks) headers["x-chunks"] = options.chunks;
|
|
45
|
+
if (options.downsample)
|
|
46
|
+
headers["x-downsample"] = JSON.stringify(options.downsample);
|
|
32
47
|
headers["x-filtered"] = "True";
|
|
48
|
+
|
|
49
|
+
this.params = {};
|
|
50
|
+
if (authHeaders["x-datamesh-auth"]) {
|
|
51
|
+
this.params["auth"] = authHeaders["x-datamesh-auth"];
|
|
52
|
+
}
|
|
53
|
+
if (authHeaders["x-datamesh-sig"]) {
|
|
54
|
+
this.params["sig"] = authHeaders["x-datamesh-sig"];
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
this.fetchOptions = { headers };
|
|
58
|
+
|
|
34
59
|
this.url = root;
|
|
35
|
-
|
|
60
|
+
const datasource = root.split("/").pop();
|
|
61
|
+
|
|
62
|
+
// Determine if caching should be used
|
|
63
|
+
if (options.nocache || typeof window === "undefined") {
|
|
36
64
|
this.cache = undefined;
|
|
37
65
|
} else {
|
|
38
66
|
this.cache = createStore("zarr", "cache");
|
|
39
67
|
}
|
|
40
|
-
|
|
68
|
+
|
|
69
|
+
// Create a cache prefix based on datasource and options
|
|
70
|
+
// Note: We don't include auth headers in the cache key to avoid leaking sensitive information
|
|
71
|
+
this.cache_prefix = hash({
|
|
72
|
+
datasource,
|
|
73
|
+
...options.parameters,
|
|
74
|
+
chunks: options.chunks,
|
|
75
|
+
downsample: options.downsample,
|
|
76
|
+
});
|
|
77
|
+
this.timeout = options.timeout || 60000;
|
|
41
78
|
}
|
|
42
79
|
|
|
43
80
|
async get(
|
|
44
|
-
item:
|
|
81
|
+
item: AbsolutePath,
|
|
45
82
|
options?: RequestInit,
|
|
46
83
|
retry = 0
|
|
47
84
|
): Promise<Uint8Array | undefined> {
|
|
@@ -49,36 +86,61 @@ export class CachedHTTPStore implements AsyncReadable {
|
|
|
49
86
|
let data = null;
|
|
50
87
|
if (this.cache) {
|
|
51
88
|
data = await get_cache(key, this.cache);
|
|
52
|
-
if (data
|
|
89
|
+
if (data) delete this._pending[key];
|
|
90
|
+
if (this._pending[key]) {
|
|
53
91
|
await delay(200);
|
|
54
|
-
|
|
92
|
+
//console.debug("Zarr pending:" + key);
|
|
93
|
+
if (retry > this.timeout) {
|
|
55
94
|
await del_cache(key, this.cache);
|
|
56
|
-
|
|
95
|
+
delete this._pending[key];
|
|
96
|
+
console.error("Zarr timeout");
|
|
97
|
+
return undefined;
|
|
57
98
|
} else {
|
|
58
|
-
return await this.get(item, options, retry +
|
|
99
|
+
return await this.get(item, options, retry + 200);
|
|
59
100
|
}
|
|
60
101
|
}
|
|
61
102
|
}
|
|
62
|
-
if (!data
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
103
|
+
if (!data) {
|
|
104
|
+
this._pending[key] = true;
|
|
105
|
+
try {
|
|
106
|
+
// Ensure we're preserving the headers from fetchOptions when making the request
|
|
107
|
+
const requestOptions = {
|
|
108
|
+
...this.fetchOptions,
|
|
109
|
+
...options,
|
|
110
|
+
headers: {
|
|
111
|
+
...(this.fetchOptions.headers || {}),
|
|
112
|
+
...(options?.headers || {}),
|
|
113
|
+
},
|
|
114
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
115
|
+
};
|
|
116
|
+
const query = new URLSearchParams(this.params).toString();
|
|
117
|
+
const response = await fetch(
|
|
118
|
+
`${this.url}${item}?${query}`,
|
|
119
|
+
requestOptions
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (response.status === 404) {
|
|
123
|
+
// Item is not found
|
|
124
|
+
if (this.cache) await del_cache(key, this.cache);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (response.status >= 400) {
|
|
128
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
129
|
+
}
|
|
130
|
+
data = new Uint8Array(await response.arrayBuffer());
|
|
131
|
+
if (this.cache) await set_cache(key, data, this.cache);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.debug("Zarr retry:" + key);
|
|
134
|
+
if (retry < this.timeout / 200) {
|
|
135
|
+
delete this._pending[key];
|
|
136
|
+
return await this.get(item, options, retry + 200);
|
|
137
|
+
}
|
|
73
138
|
if (this.cache) await del_cache(key, this.cache);
|
|
139
|
+
console.error(e);
|
|
74
140
|
return undefined;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (this.cache) await del_cache(key, this.cache);
|
|
78
|
-
throw new Error(String(response.status));
|
|
141
|
+
} finally {
|
|
142
|
+
delete this._pending[key];
|
|
79
143
|
}
|
|
80
|
-
data = new Uint8Array(await response.arrayBuffer());
|
|
81
|
-
if (this.cache) await set_cache(key, data, this.cache);
|
|
82
144
|
}
|
|
83
145
|
return data;
|
|
84
146
|
}
|
package/src/test/dataset.test.ts
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import { assertType, test, expect } from "vitest";
|
|
2
2
|
import { Dataset, IDataVar } from "../lib/datamodel";
|
|
3
3
|
import { Connector } from "../lib/connector";
|
|
4
|
-
import {
|
|
4
|
+
import { datameshTest, DATAMESH_GATEWAY, HEADERS } from "./fixtures";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
datameshTest("dataset init", async ({ dataset }) => {
|
|
7
|
+
const coordkeys = { t: "time", x: "lon", y: "lat" };
|
|
8
|
+
const ds = {
|
|
9
|
+
attributes: dataset.attrs,
|
|
10
|
+
dimensions: dataset.dims,
|
|
11
|
+
variables: {},
|
|
12
|
+
};
|
|
13
|
+
for (const v in dataset.coords) {
|
|
14
|
+
ds.variables[v] = {
|
|
15
|
+
attributes: dataset.coords[v].attrs,
|
|
16
|
+
dimensions: dataset.coords[v].dims,
|
|
17
|
+
data: dataset.coords[v].data,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
for (const v in dataset.data_vars) {
|
|
21
|
+
ds.variables[v] = {
|
|
22
|
+
attributes: dataset.data_vars[v].attrs,
|
|
23
|
+
dimensions: dataset.data_vars[v].dims,
|
|
24
|
+
data: dataset.data_vars[v].data,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const dstest = await Dataset.init(ds, coordkeys);
|
|
28
|
+
assertType<Record<string, unknown>>(dstest.attributes);
|
|
29
|
+
assertType<Record<string, IDataVar>>(dstest.variables);
|
|
30
|
+
const datatest = await dstest.variables.temperature.get();
|
|
11
31
|
expect(datatest).toBeInstanceOf(Array);
|
|
12
32
|
expect(datatest.length).toBe(10);
|
|
13
33
|
expect(datatest[0].length).toBe(30);
|
|
@@ -15,21 +35,22 @@ test("dataset init", async () => {
|
|
|
15
35
|
expect(datatest[3][4][5]).toEqual(
|
|
16
36
|
dataset.data_vars.temperature.data[3][4][5]
|
|
17
37
|
);
|
|
18
|
-
const datatest0 = await dstest.
|
|
38
|
+
const datatest0 = await dstest.variables.scalar.get();
|
|
19
39
|
expect(datatest0[0]).closeTo(10.1, 0.0001);
|
|
20
40
|
});
|
|
21
41
|
|
|
22
42
|
datameshTest(
|
|
23
43
|
"dataset zarr",
|
|
24
|
-
async ({ dataset
|
|
44
|
+
async ({ dataset }) => {
|
|
25
45
|
//Test the zarr proxy endpoint directly
|
|
26
46
|
const dstest = await Dataset.zarr(
|
|
27
47
|
DATAMESH_GATEWAY + "/zarr/" + dataset.attrs.id,
|
|
28
|
-
HEADERS
|
|
48
|
+
HEADERS,
|
|
49
|
+
{ nocache: true }
|
|
29
50
|
);
|
|
30
|
-
assertType<Record<string, unknown>>(dstest.
|
|
31
|
-
assertType<Record<string, IDataVar>>(dstest.
|
|
32
|
-
let datatest = await dstest.
|
|
51
|
+
assertType<Record<string, unknown>>(dstest.attributes);
|
|
52
|
+
assertType<Record<string, IDataVar>>(dstest.variables);
|
|
53
|
+
let datatest = await dstest.variables.temperature.get();
|
|
33
54
|
expect(datatest).toBeInstanceOf(Array);
|
|
34
55
|
expect(datatest.length).toBe(10);
|
|
35
56
|
expect(datatest[0].length).toBe(30);
|
|
@@ -37,15 +58,17 @@ datameshTest(
|
|
|
37
58
|
expect(datatest[3][4][5]).toEqual(
|
|
38
59
|
dataset.data_vars.temperature.data[3][4][5]
|
|
39
60
|
);
|
|
40
|
-
datatest = await dstest.
|
|
61
|
+
datatest = await dstest.variables.scalar.get();
|
|
41
62
|
expect(datatest[0]).closeTo(10.1, 0.0001);
|
|
42
63
|
|
|
43
64
|
//Now test with the connector
|
|
44
|
-
const datamesh = new Connector(process.env.DATAMESH_TOKEN
|
|
65
|
+
const datamesh = new Connector(process.env.DATAMESH_TOKEN, {
|
|
66
|
+
nocache: true,
|
|
67
|
+
});
|
|
45
68
|
const dstest2 = await datamesh.loadDatasource(dataset.attrs.id);
|
|
46
|
-
assertType<Record<string, unknown>>(dstest2.
|
|
47
|
-
assertType<Record<string, IDataVar>>(dstest2.
|
|
48
|
-
datatest = await
|
|
69
|
+
assertType<Record<string, unknown>>(dstest2.attributes);
|
|
70
|
+
assertType<Record<string, IDataVar>>(dstest2.variables);
|
|
71
|
+
datatest = await dstest2.variables.temperature.get();
|
|
49
72
|
expect(datatest).toBeInstanceOf(Array);
|
|
50
73
|
expect(datatest.length).toBe(10);
|
|
51
74
|
expect(datatest[0].length).toBe(30);
|
|
@@ -53,8 +76,105 @@ datameshTest(
|
|
|
53
76
|
expect(datatest[3][4][5]).toEqual(
|
|
54
77
|
dataset.data_vars.temperature.data[3][4][5]
|
|
55
78
|
);
|
|
56
|
-
datatest = await
|
|
79
|
+
datatest = await dstest2.variables.scalar.get();
|
|
57
80
|
expect(datatest[0]).closeTo(10.1, 0.0001);
|
|
58
81
|
},
|
|
59
|
-
{ timeout:
|
|
82
|
+
{ timeout: 200000 }
|
|
60
83
|
);
|
|
84
|
+
|
|
85
|
+
datameshTest("dataset fromGeojson", async () => {
|
|
86
|
+
// Test invalid FeatureCollection (no features array)
|
|
87
|
+
const invalidGeoJson = {
|
|
88
|
+
type: "FeatureCollection",
|
|
89
|
+
};
|
|
90
|
+
await expect(Dataset.fromGeojson(invalidGeoJson)).rejects.toThrow(
|
|
91
|
+
"Invalid FeatureCollection: features array is required"
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Test empty FeatureCollection
|
|
95
|
+
const emptyGeoJson = {
|
|
96
|
+
type: "FeatureCollection",
|
|
97
|
+
features: [],
|
|
98
|
+
};
|
|
99
|
+
await expect(Dataset.fromGeojson(emptyGeoJson)).rejects.toThrow(
|
|
100
|
+
"FeatureCollection contains no features"
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Test valid GeoJSON with multiple features and property types
|
|
104
|
+
const validGeoJson = {
|
|
105
|
+
type: "FeatureCollection",
|
|
106
|
+
features: [
|
|
107
|
+
{
|
|
108
|
+
type: "Feature",
|
|
109
|
+
geometry: {
|
|
110
|
+
type: "Point",
|
|
111
|
+
coordinates: [174.0, -37.0],
|
|
112
|
+
},
|
|
113
|
+
properties: {
|
|
114
|
+
time: "1970-01-01T00:00:00.000Z",
|
|
115
|
+
temperature: 15.5,
|
|
116
|
+
elevation: 100,
|
|
117
|
+
name: "Location A",
|
|
118
|
+
active: true,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: "Feature",
|
|
123
|
+
geometry: {
|
|
124
|
+
type: "LineString",
|
|
125
|
+
coordinates: [
|
|
126
|
+
[174.1, -37.1],
|
|
127
|
+
[174.2, -37.2],
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
properties: {
|
|
131
|
+
time: "1970-01-02T00:00:00.000Z",
|
|
132
|
+
temperature: 16.5,
|
|
133
|
+
elevation: 200,
|
|
134
|
+
name: "Path B",
|
|
135
|
+
active: false,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const ds = await Dataset.fromGeojson(validGeoJson);
|
|
142
|
+
|
|
143
|
+
// Test that Dataset was created with correct structure
|
|
144
|
+
expect(ds).toBeInstanceOf(Dataset);
|
|
145
|
+
assertType<Record<string, unknown>>(ds.attributes);
|
|
146
|
+
assertType<Record<string, IDataVar>>(ds.variables);
|
|
147
|
+
|
|
148
|
+
// Test that all properties were correctly extracted
|
|
149
|
+
expect(Object.keys(ds.variables)).toContain("temperature");
|
|
150
|
+
expect(Object.keys(ds.variables)).toContain("elevation");
|
|
151
|
+
expect(Object.keys(ds.variables)).toContain("name");
|
|
152
|
+
expect(Object.keys(ds.variables)).toContain("active");
|
|
153
|
+
|
|
154
|
+
// Test property values
|
|
155
|
+
const names = await ds.variables.name.get();
|
|
156
|
+
expect(names).toBeInstanceOf(Array);
|
|
157
|
+
expect(names).toHaveLength(2);
|
|
158
|
+
expect(names[0]).toBe("Location A");
|
|
159
|
+
expect(names[1]).toBe("Path B");
|
|
160
|
+
|
|
161
|
+
const active = await ds.variables.active.get();
|
|
162
|
+
expect(active).toBeInstanceOf(Array);
|
|
163
|
+
expect(active).toHaveLength(2);
|
|
164
|
+
expect(active[0]).toBe(true);
|
|
165
|
+
expect(active[1]).toBe(false);
|
|
166
|
+
|
|
167
|
+
const temp = await ds.variables.temperature.get();
|
|
168
|
+
expect(temp).toBeInstanceOf(Float32Array);
|
|
169
|
+
expect(temp).toHaveLength(2);
|
|
170
|
+
expect(temp[0]).toBe(15.5);
|
|
171
|
+
expect(temp[1]).toBe(16.5);
|
|
172
|
+
|
|
173
|
+
// Test with custom coordkeys
|
|
174
|
+
const customcoordkeys = { t: "time", g: "geometry" };
|
|
175
|
+
const dsWithcoordkeys = await Dataset.fromGeojson(
|
|
176
|
+
validGeoJson,
|
|
177
|
+
customcoordkeys
|
|
178
|
+
);
|
|
179
|
+
expect(dsWithcoordkeys.coordkeys).toEqual(customcoordkeys);
|
|
180
|
+
});
|