@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.
Files changed (50) hide show
  1. package/README.md +6 -6
  2. package/package.json +6 -8
  3. package/src/lib/connector.ts +194 -37
  4. package/src/lib/datamodel.ts +322 -148
  5. package/src/lib/datasource.ts +35 -9
  6. package/src/lib/query.ts +2 -2
  7. package/src/lib/session.ts +171 -0
  8. package/src/lib/zarr.ts +93 -31
  9. package/src/test/dataframe.test.ts +1 -1
  10. package/src/test/dataset.test.ts +139 -19
  11. package/src/test/fixtures.ts +51 -48
  12. package/src/test/query.test.ts +3 -3
  13. package/tsconfig.vitest-temp.json +13 -2
  14. package/typedoc.json +1 -1
  15. package/vite.config.ts +1 -1
  16. package/dist/README.md +0 -31
  17. package/dist/blosc-CeItQ6qj.cjs +0 -17
  18. package/dist/blosc-DaK8KnI4.js +0 -719
  19. package/dist/browser-BDe_cnOJ.cjs +0 -1
  20. package/dist/browser-CJIXy_XB.js +0 -524
  21. package/dist/chunk-INHXZS53-DiyuLb3Z.js +0 -14
  22. package/dist/chunk-INHXZS53-z3BpFH8p.cjs +0 -1
  23. package/dist/gzip-DfmsOCZR.cjs +0 -1
  24. package/dist/gzip-TMN4LZ5e.js +0 -24
  25. package/dist/index.cjs +0 -9
  26. package/dist/index.d.ts +0 -5
  27. package/dist/index.d.ts.map +0 -1
  28. package/dist/index.js +0 -11341
  29. package/dist/lib/connector.d.ts +0 -93
  30. package/dist/lib/connector.d.ts.map +0 -1
  31. package/dist/lib/datamodel.d.ts +0 -152
  32. package/dist/lib/datamodel.d.ts.map +0 -1
  33. package/dist/lib/datasource.d.ts +0 -96
  34. package/dist/lib/datasource.d.ts.map +0 -1
  35. package/dist/lib/observe.d.ts +0 -3
  36. package/dist/lib/observe.d.ts.map +0 -1
  37. package/dist/lib/query.d.ts +0 -135
  38. package/dist/lib/query.d.ts.map +0 -1
  39. package/dist/lib/zarr.d.ts +0 -20
  40. package/dist/lib/zarr.d.ts.map +0 -1
  41. package/dist/lz4-CssV0LoA.js +0 -643
  42. package/dist/lz4-PFaIsPAh.cjs +0 -15
  43. package/dist/test/fixtures.d.ts +0 -12
  44. package/dist/test/fixtures.d.ts.map +0 -1
  45. package/dist/zlib-C-RQJQaC.cjs +0 -1
  46. package/dist/zlib-DrihHfbK.js +0 -24
  47. package/dist/zstd-Cqadn9HA.js +0 -610
  48. package/dist/zstd-_xUhkGOV.cjs +0 -15
  49. package/src/docs/reverse_proxy.md +0 -0
  50. package/vite.config.ts.timestamp-1734584068599-c5119713c3c4e.mjs +0 -67
@@ -23,14 +23,40 @@ export type Coordinate =
23
23
  | "j" // Coordinate_j
24
24
  | "k"; // Coordinate_k
25
25
 
26
- export type Coordinates = {
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
- * Coordinates of the schema.
71
+ * Coordinate map of the schema.
46
72
  */
47
- coords?: Record<string, DataVariable>;
73
+ coords?: Record<string, DatameshSchema>;
48
74
 
49
75
  /**
50
76
  * Data variables of the schema.
51
77
  */
52
- data_vars: Record<string, DataVariable>;
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, unknown>;
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: Schema;
143
+ schema: DatameshSchema;
118
144
 
119
145
  /**
120
- * Coordinate mappings for the data source.
146
+ * Coordinate map for the data source.
121
147
  */
122
- coordinates: 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 | 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 | 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
- parameters?: Record<string, string | number>,
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
- if (parameters) headers["x-parameters"] = JSON.stringify(parameters);
30
- if (chunks) headers["x-chunks"] = chunks;
31
- if (downsample) headers["x-downsample"] = JSON.stringify(downsample);
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
- if (nocache) {
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
- this.cache_prefix = hash({ ...parameters, chunks, downsample });
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: string,
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 === "pending") {
89
+ if (data) delete this._pending[key];
90
+ if (this._pending[key]) {
53
91
  await delay(200);
54
- if (retry > 5 * 60) {
92
+ //console.debug("Zarr pending:" + key);
93
+ if (retry > this.timeout) {
55
94
  await del_cache(key, this.cache);
56
- throw new Error("Zarr timeout");
95
+ delete this._pending[key];
96
+ console.error("Zarr timeout");
97
+ return undefined;
57
98
  } else {
58
- return await this.get(item, options, retry + 1);
99
+ return await this.get(item, options, retry + 200);
59
100
  }
60
101
  }
61
102
  }
62
- if (!data || !this.cache) {
63
- if (this.cache) set_cache(key, "pending", this.cache);
64
- //console.log(`${this.url}/${item}`);
65
- //console.log(this.fetchOptions.headers);
66
- const response = await fetch(`${this.url}${item}`, {
67
- ...this.fetchOptions,
68
- ...options,
69
- });
70
-
71
- if (response.status === 404) {
72
- // Item is not found
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
- } else if (response.status !== 200) {
76
- // Item is found but there was an error
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
  }
@@ -1,4 +1,4 @@
1
- import { test, expect } from "vitest";
1
+ import { expect } from "vitest";
2
2
  import { Connector } from "../lib/connector";
3
3
  import { datameshTest } from "./fixtures";
4
4
 
@@ -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 { dataset, datameshTest, DATAMESH_GATEWAY, HEADERS } from "./fixtures";
4
+ import { datameshTest, DATAMESH_GATEWAY, HEADERS } from "./fixtures";
5
5
 
6
- test("dataset init", async () => {
7
- const dstest = await Dataset.init(dataset);
8
- assertType<Record<string, unknown>>(dstest.attrs);
9
- assertType<Record<string, IDataVar>>(dstest.data_vars);
10
- const datatest = await dstest.data_vars.temperature.get();
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.data_vars.scalar.get();
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: 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.attrs);
31
- assertType<Record<string, IDataVar>>(dstest.data_vars);
32
- let datatest = await dstest.data_vars.temperature.get();
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.data_vars.scalar.get();
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.attrs);
47
- assertType<Record<string, IDataVar>>(dstest2.data_vars);
48
- datatest = await dstest.data_vars.temperature.get();
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 dstest.data_vars.scalar.get();
79
+ datatest = await dstest2.variables.scalar.get();
57
80
  expect(datatest[0]).closeTo(10.1, 0.0001);
58
81
  },
59
- { timeout: 100000 }
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
+ });