@jskit-ai/json-rest-api-core 0.1.91 → 0.1.92

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,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/json-rest-api-core",
4
- version: "0.1.91",
4
+ version: "0.1.92",
5
5
  kind: "runtime",
6
6
  description: "Shared internal json-rest-api host runtime with autofilter, query-projection, and row-policy support.",
7
7
  dependsOn: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/json-rest-api-core",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -11,6 +11,6 @@
11
11
  "dependencies": {
12
12
  "hooked-api": "1.x.x",
13
13
  "json-rest-api": "^1.0.27",
14
- "@jskit-ai/kernel": "0.1.146"
14
+ "@jskit-ai/kernel": "0.1.147"
15
15
  }
16
16
  }
@@ -54,6 +54,8 @@ const JSON_REST_RESERVED_QUERY_KEYS = Object.freeze(new Set([
54
54
  "fields"
55
55
  ]));
56
56
  const JSON_REST_CALENDAR_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
57
+ const JSON_REST_DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
58
+ const JSON_REST_DATABASE_UTC_DATE_TIME_PATTERN = /^(\d{4}-\d{2}-\d{2}) ((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?)$/u;
57
59
 
58
60
  function isPlainJsonRestObject(value) {
59
61
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -139,6 +141,20 @@ function serializeJsonRestCalendarDate(value) {
139
141
  return canonical;
140
142
  }
141
143
 
144
+ function resolveCanonicalDateTime(value) {
145
+ if (value instanceof Date) {
146
+ return Number.isNaN(value.getTime()) ? "" : value.toISOString();
147
+ }
148
+
149
+ const normalized = typeof value === "string" ? value.trim() : "";
150
+ if (JSON_REST_DATE_TIME_PATTERN.test(normalized)) {
151
+ return normalized;
152
+ }
153
+
154
+ const databaseMatch = JSON_REST_DATABASE_UTC_DATE_TIME_PATTERN.exec(normalized);
155
+ return databaseMatch ? `${databaseMatch[1]}T${databaseMatch[2]}Z` : "";
156
+ }
157
+
142
158
  function applyJsonRestCalendarDateWriteSerializers(scopeOptions = {}) {
143
159
  const schema = normalizeJsonRestObject(scopeOptions.schema);
144
160
  for (const fieldDefinition of Object.values(schema)) {
@@ -160,7 +176,7 @@ function applyJsonRestCalendarDateWriteSerializers(scopeOptions = {}) {
160
176
  }
161
177
  }
162
178
 
163
- function normalizeJsonRestCalendarDateEntry(entry = null, scopes = {}) {
179
+ function normalizeJsonRestTemporalEntry(entry = null, scopes = {}) {
164
180
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
165
181
  return entry;
166
182
  }
@@ -170,15 +186,16 @@ function normalizeJsonRestCalendarDateEntry(entry = null, scopes = {}) {
170
186
  );
171
187
  const attributes = normalizeJsonRestObject(entry.attributes);
172
188
  for (const [fieldName, definition] of Object.entries(schema)) {
173
- if (
174
- normalizeJsonRestText(definition?.type).toLowerCase() !== "date" ||
175
- !Object.hasOwn(attributes, fieldName) ||
176
- attributes[fieldName] == null
177
- ) {
189
+ if (!Object.hasOwn(attributes, fieldName) || attributes[fieldName] == null) {
178
190
  continue;
179
191
  }
180
192
 
181
- const canonical = resolveCanonicalCalendarDate(attributes[fieldName]);
193
+ const fieldType = normalizeJsonRestText(definition?.type).toLowerCase();
194
+ const canonical = fieldType === "date"
195
+ ? resolveCanonicalCalendarDate(attributes[fieldName])
196
+ : fieldType === "datetime"
197
+ ? resolveCanonicalDateTime(attributes[fieldName])
198
+ : "";
182
199
  if (canonical) {
183
200
  attributes[fieldName] = canonical;
184
201
  }
@@ -187,31 +204,31 @@ function normalizeJsonRestCalendarDateEntry(entry = null, scopes = {}) {
187
204
  return entry;
188
205
  }
189
206
 
190
- function normalizeJsonRestCalendarDateDocument(document = null, scopes = {}) {
207
+ function normalizeJsonRestTemporalDocument(document = null, scopes = {}) {
191
208
  if (!document || typeof document !== "object" || Array.isArray(document)) {
192
209
  return document;
193
210
  }
194
211
 
195
212
  const data = Array.isArray(document.data) ? document.data : [document.data];
196
213
  for (const entry of data) {
197
- normalizeJsonRestCalendarDateEntry(entry, scopes);
214
+ normalizeJsonRestTemporalEntry(entry, scopes);
198
215
  }
199
216
  for (const entry of Array.isArray(document.included) ? document.included : []) {
200
- normalizeJsonRestCalendarDateEntry(entry, scopes);
217
+ normalizeJsonRestTemporalEntry(entry, scopes);
201
218
  }
202
219
  return document;
203
220
  }
204
221
 
205
- const JsonRestCalendarDatePlugin = Object.freeze({
206
- name: "jskit-calendar-date",
222
+ const JsonRestTemporalPlugin = Object.freeze({
223
+ name: "jskit-temporal",
207
224
  dependencies: ["rest-api"],
208
225
  install({ addHook, scopes }) {
209
226
  addHook("finish", "normalizeCalendarDateDocuments", {}, ({ context }) => {
210
227
  if (context?.record) {
211
- normalizeJsonRestCalendarDateDocument(context.record, scopes);
228
+ normalizeJsonRestTemporalDocument(context.record, scopes);
212
229
  }
213
230
  if (context?.responseRecord) {
214
- normalizeJsonRestCalendarDateDocument(context.responseRecord, scopes);
231
+ normalizeJsonRestTemporalDocument(context.responseRecord, scopes);
215
232
  }
216
233
  });
217
234
  }
@@ -815,7 +832,7 @@ async function createJsonRestApiHost({ knex }) {
815
832
  },
816
833
  presets: JSON_REST_AUTOFILTER_PRESETS
817
834
  });
818
- await api.use(JsonRestCalendarDatePlugin);
835
+ await api.use(JsonRestTemporalPlugin);
819
836
 
820
837
  return api;
821
838
  }
@@ -641,7 +641,7 @@ test("createJsonRestApiHost installs json-rest-api query projections", async ()
641
641
  assert.equal(typeof api.resources.projectionContacts.vars.queryFields.displayName.select, "function");
642
642
  });
643
643
 
644
- test("createJsonRestApiHost returns calendar dates as YYYY-MM-DD without changing date-time or time fields", async () => {
644
+ test("createJsonRestApiHost returns JSON-native temporal values from database records", async () => {
645
645
  const fakeKnex = Object.assign(() => {}, {
646
646
  client: {
647
647
  config: {
@@ -697,7 +697,7 @@ test("createJsonRestApiHost returns calendar dates as YYYY-MM-DD without changin
697
697
  });
698
698
 
699
699
  assert.equal(record.data.attributes.publishedOn, "2024-02-29");
700
- assert.equal(record.data.attributes.scheduledAt, scheduledAt);
700
+ assert.equal(record.data.attributes.scheduledAt, "2024-02-29T12:34:56.000Z");
701
701
  assert.equal(record.data.attributes.opensAt, "09:45:00");
702
702
  assert.equal(record.included[0].attributes.observedOn, "2024-03-01");
703
703
  });
@@ -769,6 +769,38 @@ test("calendar date writes and responses keep leap day across server time zones"
769
769
  }
770
770
  });
771
771
 
772
+ test("createJsonRestApiHost maps UTC database datetimes to RFC 3339 strings", async () => {
773
+ const fakeKnex = Object.assign(() => {}, {
774
+ client: { config: { client: "sqlite3" } },
775
+ async raw() {
776
+ return [{ version: "3.35.5" }];
777
+ },
778
+ transaction() {}
779
+ });
780
+ const api = await createJsonRestApiHost({ knex: fakeKnex });
781
+
782
+ await api.addResource("jobs", createJsonRestResourceScopeOptions({
783
+ tableName: "jobs",
784
+ schema: {
785
+ id: { type: "id", primary: true },
786
+ scheduledAt: { type: "dateTime" }
787
+ }
788
+ }));
789
+ const record = {
790
+ data: {
791
+ type: "jobs",
792
+ id: "1",
793
+ attributes: {
794
+ scheduledAt: "2024-02-29 12:34:56.123"
795
+ }
796
+ }
797
+ };
798
+
799
+ await api.runHooks("finish", { record });
800
+
801
+ assert.equal(record.data.attributes.scheduledAt, "2024-02-29T12:34:56.123Z");
802
+ });
803
+
772
804
  test("returnNullWhenJsonRestResourceMissing only swallows missing-resource errors", async () => {
773
805
  await assert.doesNotReject(async () => {
774
806
  const result = await returnNullWhenJsonRestResourceMissing(async () => {