@primate/core 0.9.5 → 0.9.7

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.
@@ -2,6 +2,7 @@ import type Mode from "#Mode";
2
2
  import type RequestView from "#request/RequestView";
3
3
  import type { Dict } from "@rcompat/type";
4
4
  type ClientData<T extends Dict = Dict> = {
5
+ frontend: string;
5
6
  view: string;
6
7
  request: RequestView;
7
8
  csr: boolean;
@@ -1,5 +1,5 @@
1
1
  import type { Updater } from "#client/root";
2
2
  import type { Dict } from "@rcompat/type";
3
3
  export default _default;
4
- declare function _default<T extends Dict>(u: Updater<T>): void;
4
+ declare function _default<T extends Dict>(frontend: string, u: Updater<T>): void;
5
5
  //# sourceMappingURL=boot.d.ts.map
@@ -3,8 +3,8 @@ import root from "#client/root";
3
3
  import storage from "#client/storage";
4
4
  import submit from "#client/submit";
5
5
  import http from "@rcompat/http";
6
- export default (u) => {
7
- root.set(u);
6
+ export default (frontend, u) => {
7
+ root.set(u, frontend);
8
8
  const { location, history } = globalThis;
9
9
  if (document.contentType === "application/json") {
10
10
  location.reload();
@@ -11,7 +11,7 @@ import type ValidateUpdater from "#client/ValidateUpdater";
11
11
  import type ValidationError from "#client/ValidationError";
12
12
  import type ViewResponse from "#client/ViewResponse";
13
13
  declare const client: {
14
- boot: <T extends import("@rcompat/type").Dict>(u: import("./root.js").Updater<T>) => void;
14
+ boot: <T extends import("@rcompat/type").Dict>(frontend: string, u: import("./root.js").Updater<T>) => void;
15
15
  navigate: (href: string, event?: Event) => Promise<void>;
16
16
  submit: typeof submit;
17
17
  createForm: typeof createForm;
@@ -21,8 +21,14 @@ async function goto(target, state = false, after) {
21
21
  const path = target.pathname + target.search;
22
22
  const { requested, response } = await transport.refetch(path, { headers });
23
23
  if (transport.is_json(response)) {
24
- if (response.ok)
25
- root.update(await response.json());
24
+ if (response.ok) {
25
+ const data = await response.json();
26
+ if (!root.same(data)) {
27
+ location.assign(requested.toString() + target.hash);
28
+ return;
29
+ }
30
+ root.update(data);
31
+ }
26
32
  if (state) {
27
33
  storage.new({ hash: location.hash, pathname: location.pathname, scrollTop });
28
34
  history.pushState({}, "", `${path}${target.hash}`);
@@ -2,7 +2,8 @@ import type ClientData from "#client/Data";
2
2
  import type { Dict } from "@rcompat/type";
3
3
  export type Updater<T extends Dict> = (json: ClientData<T>, after?: () => void) => void;
4
4
  declare const root: {
5
- set(updater: Updater<any>): void;
5
+ set(updater: Updater<any>, name: string): void;
6
+ same(data: ClientData): boolean;
6
7
  update(data: any, after?: () => void): void;
7
8
  };
8
9
  export default root;
@@ -1,7 +1,12 @@
1
1
  let current;
2
+ let frontend;
2
3
  const root = {
3
- set(updater) {
4
+ set(updater, name) {
4
5
  current = updater;
6
+ frontend = name;
7
+ },
8
+ same(data) {
9
+ return data.frontend === frontend;
5
10
  },
6
11
  update(data, after) {
7
12
  current(data, after);
@@ -34,6 +34,7 @@ declare function sort_invalid_value(field: string, value: unknown): import("@rco
34
34
  declare function select_empty(): import("@rcompat/error").TemplateError;
35
35
  declare function select_invalid(): import("@rcompat/error").TemplateError;
36
36
  declare function limit_invalid(): import("@rcompat/error").TemplateError;
37
+ declare function first_limit_invalid(): import("@rcompat/error").TemplateError;
37
38
  declare function select_invalid_value(index: number, x: unknown): import("@rcompat/error").TemplateError;
38
39
  declare function where_required(): import("@rcompat/error").TemplateError;
39
40
  declare function where_invalid(): import("@rcompat/error").TemplateError;
@@ -94,6 +95,7 @@ declare const errors: {
94
95
  where_invalid_value: typeof where_invalid_value;
95
96
  set_empty: typeof set_empty;
96
97
  limit_invalid: typeof limit_invalid;
98
+ first_limit_invalid: typeof first_limit_invalid;
97
99
  offset_invalid: typeof offset_invalid;
98
100
  offset_requires_limit: typeof offset_requires_limit;
99
101
  relation_unknown: typeof relation_unknown;
@@ -150,6 +152,7 @@ export declare const Code: {
150
152
  where_invalid_value: "where_invalid_value";
151
153
  set_empty: "set_empty";
152
154
  limit_invalid: "limit_invalid";
155
+ first_limit_invalid: "first_limit_invalid";
153
156
  offset_invalid: "offset_invalid";
154
157
  offset_requires_limit: "offset_requires_limit";
155
158
  relation_unknown: "relation_unknown";
@@ -152,6 +152,9 @@ function select_invalid() {
152
152
  function limit_invalid() {
153
153
  return t `invalid limit`;
154
154
  }
155
+ function first_limit_invalid() {
156
+ return t `${"first"} does not accept ${"limit"}`;
157
+ }
155
158
  function select_invalid_value(index, x) {
156
159
  return t `select[${index}]: must be string, received ${kind_of(x)}`;
157
160
  }
@@ -185,6 +188,7 @@ const QUERY = error.coded({
185
188
  where_invalid_value,
186
189
  set_empty,
187
190
  limit_invalid,
191
+ first_limit_invalid,
188
192
  offset_invalid,
189
193
  offset_requires_limit,
190
194
  });
@@ -1498,6 +1498,24 @@ export default (db) => {
1498
1498
  assert(yes.length).equals(1);
1499
1499
  assert(yes[0].name).equals("MiXeDCase");
1500
1500
  });
1501
+ $user("first: returns first matching record", async (assert) => {
1502
+ await User.insert({ name: "FirstLow", age: 1 });
1503
+ await User.insert({ name: "FirstHigh", age: 2 });
1504
+ const user = await User.first({
1505
+ where: { name: { $like: "First%" } },
1506
+ sort: { age: "desc" },
1507
+ });
1508
+ assert(user?.name).equals("FirstHigh");
1509
+ });
1510
+ $user("first: returns undefined when no record matches", async (assert) => {
1511
+ const user = await User.first({ where: { name: "MissingFirst" } });
1512
+ assert(user).undefined();
1513
+ });
1514
+ $user$("first: rejects limit", async (assert) => {
1515
+ await throws(assert, Code.first_limit_invalid, () => {
1516
+ return User.first({ limit: 10 });
1517
+ });
1518
+ });
1501
1519
  $user("find: $ilike is case-insensitive", async (assert) => {
1502
1520
  await User.insert({ name: "MiXeDCase2" });
1503
1521
  const rows = await User.find({ where: { name: { $ilike: "mixed%" } } });
@@ -18,8 +18,9 @@ export default function frontend_module(init) {
18
18
  const conditions = init.conditions ?? [];
19
19
  const schema = base_schema.extend(init.schema ?? {});
20
20
  async function normalize(path) {
21
- const file = fs.ref(path);
22
- const basename = path.slice(0, -file.fullExtension.length);
21
+ const extension = init.extensions.find(extension => path.endsWith(extension))
22
+ ?? fs.ref(path).fullExtension;
23
+ const basename = path.slice(0, -extension.length);
23
24
  return `p_${await hash(`${basename}.${module_name}`)}`;
24
25
  }
25
26
  return (input) => {
@@ -57,7 +58,7 @@ export default function frontend_module(init) {
57
58
  if (app_asset === undefined)
58
59
  throw E.frontend_missing_app_js();
59
60
  const app_script = `<script type="module" src="${app_asset.src}"></script>`;
60
- const props = JSON.stringify({ frontend: init.name, ...client });
61
+ const props = JSON.stringify(client);
61
62
  const hydrated = await inline(props, http.MIME.APPLICATION_JSON, "hydration");
62
63
  const script_src = [hydrated.integrity];
63
64
  return {
@@ -83,6 +84,7 @@ export default function frontend_module(init) {
83
84
  }
84
85
  : { props, request: $request };
85
86
  const client = {
87
+ frontend: init.name,
86
88
  view: init.layouts ? "root" : await normalize(view),
87
89
  csr,
88
90
  ssr: ssr(),
@@ -206,11 +208,11 @@ export default function frontend_module(init) {
206
208
  build.onLoad({ filter: views_filter }, async () => {
207
209
  const views = await views_base.files({
208
210
  recursive: true,
209
- filter: c => extensions.includes(c.fullExtension),
211
+ filter: c => extensions.some(ext => c.path.endsWith(ext)),
210
212
  });
211
213
  const pages = await pages_base.files({
212
214
  recursive: true,
213
- filter: c => extensions.includes(c.fullExtension),
215
+ filter: c => extensions.some(ext => c.path.endsWith(ext)),
214
216
  });
215
217
  let contents = "";
216
218
  for (const view of views) {
@@ -5,5 +5,5 @@ import type ResponseFunction from "#response/ResponseFunction";
5
5
  * @param options response options
6
6
  * @return Response rendering function
7
7
  */
8
- export default function json<const T>(body: T, init?: ResponseInit): ResponseFunction<never, T>;
8
+ export default function json<T>(body: T, init?: ResponseInit): ResponseFunction<never, T>;
9
9
  //# sourceMappingURL=json.d.ts.map
@@ -179,6 +179,17 @@ export default class Store<T extends StoreInput, N extends string = string> impl
179
179
  offset?: number;
180
180
  with?: W;
181
181
  }): Promise<WithRelations<Projected<Schema<T>, S>, ExtractRelations<T>, W>[]>;
182
+ /**
183
+ * Find the first matching record.
184
+ */
185
+ first<const S extends Select<Schema<T>> | undefined = undefined, const W extends WithInput<ExtractRelations<T>> | undefined = undefined>(options?: {
186
+ where?: Where<T>;
187
+ select?: S;
188
+ sort?: Sort<Schema<T>>;
189
+ offset?: number;
190
+ with?: W;
191
+ limit?: never;
192
+ }): Promise<WithRelations<Projected<Schema<T>, S>, ExtractRelations<T>, W> | undefined>;
182
193
  toJSON(): {
183
194
  type: "object";
184
195
  properties: Dict<import("pema").Serialized>;
@@ -590,6 +590,16 @@ export default class Store {
590
590
  });
591
591
  return result;
592
592
  }
593
+ /**
594
+ * Find the first matching record.
595
+ */
596
+ async first(options) {
597
+ guard_options(options, ["where", "select", "sort", "offset", "with", "limit"]);
598
+ if (is.defined(options?.limit))
599
+ throw E.first_limit_invalid();
600
+ const [first] = await this.find({ ...options, limit: 1 });
601
+ return first;
602
+ }
593
603
  toJSON() {
594
604
  return this.#schema.toJSON();
595
605
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primate/core",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "The universal web framework",
5
5
  "homepage": "https://primate.run",
6
6
  "bugs": "https://github.com/primate-run/primate/issues",