@primate/core 0.6.2 → 0.7.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 (185) hide show
  1. package/lib/private/App.d.ts +76 -149
  2. package/lib/private/App.js +22 -6
  3. package/lib/private/Flags.d.ts +5 -3
  4. package/lib/private/Flags.js +4 -2
  5. package/lib/private/app/Facade.d.ts +77 -156
  6. package/lib/private/app/Facade.js +4 -1
  7. package/lib/private/build/App.d.ts +4 -2
  8. package/lib/private/build/App.js +13 -4
  9. package/lib/private/build/client/index.js +24 -10
  10. package/lib/private/build/client/plugin/routes.d.ts +4 -0
  11. package/lib/private/build/client/plugin/routes.js +74 -0
  12. package/lib/private/build/hook.js +14 -6
  13. package/lib/private/build/index.d.ts +3 -2
  14. package/lib/private/build/index.js +9 -15
  15. package/lib/private/build/preclient/index.d.ts +3 -0
  16. package/lib/private/build/preclient/index.js +69 -0
  17. package/lib/private/build/preclient/plugin/routes.d.ts +4 -0
  18. package/lib/private/build/preclient/plugin/routes.js +44 -0
  19. package/lib/private/build/server/index.js +8 -5
  20. package/lib/private/build/server/plugin/assets.js +3 -3
  21. package/lib/private/build/server/plugin/native-addons.js +6 -8
  22. package/lib/private/build/server/plugin/node-imports.js +5 -7
  23. package/lib/private/build/server/plugin/route-client.d.ts +4 -0
  24. package/lib/private/build/server/plugin/route-client.js +110 -0
  25. package/lib/private/build/server/plugin/route.js +3 -10
  26. package/lib/private/build/server/plugin/virtual-pages.js +3 -3
  27. package/lib/private/build/server/plugin/virtual-routes.d.ts +2 -1
  28. package/lib/private/build/server/plugin/virtual-routes.js +27 -8
  29. package/lib/private/build/server/plugin/wasm.js +3 -2
  30. package/lib/private/client/Data.d.ts +1 -1
  31. package/lib/private/client/boot.js +2 -2
  32. package/lib/private/client/create-form.d.ts +11 -1
  33. package/lib/private/client/create-form.js +21 -3
  34. package/lib/private/client/index.d.ts +2 -2
  35. package/lib/private/client/navigate.d.ts +1 -0
  36. package/lib/private/client/navigate.js +7 -6
  37. package/lib/private/client/submit.d.ts +2 -1
  38. package/lib/private/client/submit.js +8 -7
  39. package/lib/private/client/{http.d.ts → transport.d.ts} +3 -3
  40. package/lib/private/client/{http.js → transport.js} +7 -9
  41. package/lib/private/config/schema.d.ts +5 -13
  42. package/lib/private/config/schema.js +1 -5
  43. package/lib/private/db/DB.d.ts +3 -1
  44. package/lib/private/db/MemoryDB.d.ts +5 -2
  45. package/lib/private/db/MemoryDB.js +33 -19
  46. package/lib/private/db/SQLDB.d.ts +23 -0
  47. package/lib/private/db/SQLDB.js +2 -0
  48. package/lib/private/db/errors.d.ts +74 -7
  49. package/lib/private/db/errors.js +31 -7
  50. package/lib/private/db/migrate/apply.js +8 -9
  51. package/lib/private/db/migrate/bundle.js +3 -2
  52. package/lib/private/db/migrate/create.js +18 -20
  53. package/lib/private/db/migrate/status.js +9 -10
  54. package/lib/private/db/migrate/store.d.ts +3 -3
  55. package/lib/private/db/migrate/store.js +5 -5
  56. package/lib/private/db/test.js +256 -115
  57. package/lib/private/db/testSQL.d.ts +50 -0
  58. package/lib/private/db/testSQL.js +196 -0
  59. package/lib/private/errors.d.ts +66 -9
  60. package/lib/private/errors.js +37 -16
  61. package/lib/private/frontend.d.ts +4 -4
  62. package/lib/private/frontend.js +11 -8
  63. package/lib/private/i18n/errors.d.ts +7 -1
  64. package/lib/private/i18n/errors.js +1 -1
  65. package/lib/private/i18n/index/types.d.ts +1 -1
  66. package/lib/private/i18n/locale.d.ts +1 -1
  67. package/lib/private/i18n/module.js +6 -5
  68. package/lib/private/index.d.ts +10 -2
  69. package/lib/private/index.js +13 -1
  70. package/lib/private/logger.d.ts +24 -0
  71. package/lib/private/logger.js +66 -0
  72. package/lib/private/module/Setup.d.ts +3 -0
  73. package/lib/private/module/create.d.ts +2 -1
  74. package/lib/private/module/create.js +4 -0
  75. package/lib/private/paths.d.ts +2 -3
  76. package/lib/private/paths.js +6 -12
  77. package/lib/private/request/ContentType.d.ts +3 -0
  78. package/lib/private/request/ContentType.js +2 -0
  79. package/lib/private/request/RequestBag.d.ts +2 -18
  80. package/lib/private/request/RequestBag.js +4 -16
  81. package/lib/private/request/RequestBody.d.ts +20 -28
  82. package/lib/private/request/RequestBody.js +63 -86
  83. package/lib/private/request/RequestFacade.d.ts +2 -2
  84. package/lib/private/request/handle.js +2 -2
  85. package/lib/private/request/parse.js +2 -4
  86. package/lib/private/request/route.js +15 -8
  87. package/lib/private/response/binary.d.ts +2 -2
  88. package/lib/private/response/binary.js +6 -6
  89. package/lib/private/response/error.js +6 -2
  90. package/lib/private/response/json.js +2 -2
  91. package/lib/private/response/null.d.ts +3 -0
  92. package/lib/private/response/null.js +6 -0
  93. package/lib/private/response/redirect.js +4 -3
  94. package/lib/private/response/respond.js +4 -4
  95. package/lib/private/response/sse.js +2 -2
  96. package/lib/private/response/text.js +2 -2
  97. package/lib/private/route/ContentTypeMap.d.ts +10 -0
  98. package/lib/private/route/ContentTypeMap.js +2 -0
  99. package/lib/private/route/Handler.d.ts +3 -2
  100. package/lib/private/route/NarrowedRequest.d.ts +23 -0
  101. package/lib/private/route/NarrowedRequest.js +2 -0
  102. package/lib/private/route/Options.d.ts +6 -1
  103. package/lib/private/route/Path.d.ts +2 -2
  104. package/lib/private/route/hook.d.ts +3 -1
  105. package/lib/private/route/hook.js +1 -2
  106. package/lib/private/route/router.d.ts +7 -11
  107. package/lib/private/route/router.js +13 -20
  108. package/lib/private/route.client.d.ts +36 -0
  109. package/lib/private/route.client.js +8 -0
  110. package/lib/private/route.d.ts +21 -5
  111. package/lib/private/route.js +21 -5
  112. package/lib/private/serve/App.d.ts +1 -2
  113. package/lib/private/serve/App.js +64 -58
  114. package/lib/private/serve/Init.d.ts +2 -0
  115. package/lib/private/serve/dev-module.js +2 -3
  116. package/lib/private/serve/index.js +5 -6
  117. package/lib/private/server/TAG.d.ts +1 -1
  118. package/lib/private/server/TAG.js +1 -1
  119. package/lib/private/session/index.d.ts +1 -1
  120. package/lib/private/session/index.js +3 -2
  121. package/lib/private/session/module.js +3 -3
  122. package/lib/private/session/schema.d.ts +3 -3
  123. package/lib/private/session/schema.js +4 -3
  124. package/lib/private/store/ExtractRelation.d.ts +7 -0
  125. package/lib/private/store/ExtractRelation.js +2 -0
  126. package/lib/private/store/ExtractSchema.d.ts +10 -0
  127. package/lib/private/{orm → store}/ForeignKey.d.ts +1 -1
  128. package/lib/private/store/Init.d.ts +13 -0
  129. package/lib/private/store/Init.js +2 -0
  130. package/lib/private/{orm/store.d.ts → store/Store.d.ts} +50 -50
  131. package/lib/private/{orm/store.js → store/Store.js} +163 -107
  132. package/lib/private/store/StoreInput.d.ts +11 -0
  133. package/lib/private/store/key.d.ts +8 -0
  134. package/lib/private/store/key.js +8 -0
  135. package/lib/private/{orm → store}/parse.d.ts +3 -3
  136. package/lib/private/{orm → store}/parse.js +7 -2
  137. package/lib/private/store/relation.d.ts +29 -0
  138. package/lib/private/store/relation.js +26 -0
  139. package/lib/private/store.d.ts +24 -0
  140. package/lib/private/store.js +10 -0
  141. package/lib/public/db/errors.d.ts +1 -1
  142. package/lib/public/db/errors.js +1 -1
  143. package/lib/public/db/testSQL.d.ts +2 -0
  144. package/lib/public/db/testSQL.js +2 -0
  145. package/lib/public/db.d.ts +1 -0
  146. package/lib/public/index.d.ts +1 -0
  147. package/lib/public/index.js +1 -1
  148. package/lib/public/response.d.ts +6 -6
  149. package/lib/public/response.js +4 -1
  150. package/lib/public/route.client.d.ts +2 -0
  151. package/lib/public/route.client.js +2 -0
  152. package/lib/public/store.d.ts +2 -0
  153. package/lib/public/store.js +2 -0
  154. package/package.json +24 -17
  155. package/lib/private/bye.d.ts +0 -3
  156. package/lib/private/bye.js +0 -4
  157. package/lib/private/log.d.ts +0 -20
  158. package/lib/private/log.js +0 -47
  159. package/lib/private/orm/ExtractSchema.d.ts +0 -9
  160. package/lib/private/orm/StoreInput.d.ts +0 -10
  161. package/lib/private/orm/key.d.ts +0 -8
  162. package/lib/private/orm/key.js +0 -8
  163. package/lib/private/orm/relation.d.ts +0 -43
  164. package/lib/private/orm/relation.js +0 -26
  165. package/lib/private/request/Verb.d.ts +0 -4
  166. package/lib/private/request/Verb.js +0 -2
  167. package/lib/private/request/verbs.d.ts +0 -3
  168. package/lib/private/request/verbs.js +0 -12
  169. package/lib/private/route/wrap.d.ts +0 -2
  170. package/lib/private/route/wrap.js +0 -12
  171. package/lib/public/log.d.ts +0 -2
  172. package/lib/public/log.js +0 -2
  173. package/lib/public/orm/key.d.ts +0 -2
  174. package/lib/public/orm/key.js +0 -2
  175. package/lib/public/orm/relation.d.ts +0 -2
  176. package/lib/public/orm/relation.js +0 -2
  177. package/lib/public/orm/store.d.ts +0 -2
  178. package/lib/public/orm/store.js +0 -2
  179. package/lib/public/request/verbs.d.ts +0 -2
  180. package/lib/public/request/verbs.js +0 -2
  181. /package/lib/private/{orm → store}/ExtractSchema.js +0 -0
  182. /package/lib/private/{orm → store}/ForeignKey.js +0 -0
  183. /package/lib/private/{orm → store}/PrimaryKey.d.ts +0 -0
  184. /package/lib/private/{orm → store}/PrimaryKey.js +0 -0
  185. /package/lib/private/{orm → store}/StoreInput.js +0 -0
@@ -1,7 +1,6 @@
1
1
  import E from "#errors";
2
- import wrap from "#route/wrap";
3
2
  import fs from "@rcompat/fs";
4
- const contents = "export default {};";
3
+ const empty = "export default {};";
5
4
  export default function plugin_server_route(app) {
6
5
  const path_routes = app.path.routes;
7
6
  return {
@@ -28,23 +27,17 @@ export default function plugin_server_route(app) {
28
27
  if (!file || !extension) {
29
28
  throw E.build_missing_route(relative, path_routes);
30
29
  }
31
- // normalise "routes/foo.ext" -> "foo" for router
32
- const relative_from_routes = file.path.split("routes").pop();
33
- const no_extensions = relative_from_routes
34
- .replace(/^[\\/]/, "")
35
- .slice(0, -extension.length);
36
- const route_path = no_extensions.replace(/\\/g, "/");
37
30
  const resolveDir = file.directory.path;
38
31
  const watchFiles = [file.path];
39
32
  const binder = app.binder(file);
40
33
  if (!binder)
41
- return { contents, loader: "js", resolveDir, watchFiles };
34
+ return { contents: empty, loader: "js", resolveDir, watchFiles };
42
35
  const compiled = await binder(file, {
43
36
  build: { id: app.id },
44
37
  context: "routes",
45
38
  });
46
39
  return {
47
- contents: wrap(compiled, route_path, app.id),
40
+ contents: compiled,
48
41
  loader: extension === ".ts" ? "ts" : "js",
49
42
  resolveDir,
50
43
  watchFiles,
@@ -1,5 +1,5 @@
1
- import fs from "@rcompat/fs";
2
- const core_pkg = await fs.project.root(import.meta.dirname);
1
+ import runtime from "@rcompat/runtime";
2
+ const core = await runtime.projectRoot(import.meta.dirname);
3
3
  export default function plugin_server_virtual_pages(app) {
4
4
  return {
5
5
  name: "primate/server/virtual/pages",
@@ -9,7 +9,7 @@ export default function plugin_server_virtual_pages(app) {
9
9
  });
10
10
  build.onLoad({ filter: /.*/, namespace: "primate-pages" }, async () => {
11
11
  const filter = /^.*\.html$/ui;
12
- const defaults = core_pkg.join("lib", "private", "defaults");
12
+ const defaults = core.join("lib", "private", "defaults");
13
13
  const pages = {};
14
14
  for (const file of await defaults.files({ filter })) {
15
15
  pages[file.name] = file;
@@ -1,4 +1,5 @@
1
1
  import type BuildApp from "#build/App";
2
2
  import type { Plugin } from "esbuild";
3
- export default function plugin_server_virtual_routes(app: BuildApp): Plugin;
3
+ declare function plugin_server_virtual_routes(app: BuildApp): Plugin;
4
+ export default plugin_server_virtual_routes;
4
5
  //# sourceMappingURL=virtual-routes.d.ts.map
@@ -1,8 +1,15 @@
1
- export default function plugin_server_virtual_routes(app) {
1
+ function is_hook_file(p) {
2
+ const basename = p.split("/").at(-1) ?? p;
3
+ return basename === "+hook" || basename.startsWith("+hook.");
4
+ }
5
+ function plugin_server_virtual_routes(app) {
2
6
  const extension_pattern = new RegExp(`(${app.extensions.map(e => e.replace(".", "\\.")).join("|")})$`);
3
- const is_route_file = (f) => !f.name.endsWith("~") &&
4
- !f.name.startsWith(".") &&
5
- extension_pattern.test(f.path);
7
+ function is_route_file(f) {
8
+ return !f.name.endsWith("~") &&
9
+ !f.name.startsWith(".") &&
10
+ !f.name.startsWith("-") &&
11
+ extension_pattern.test(f.path);
12
+ }
6
13
  return {
7
14
  name: "primate/server/virtual/routes",
8
15
  setup(build) {
@@ -16,13 +23,24 @@ export default function plugin_server_virtual_routes(app) {
16
23
  recursive: true,
17
24
  });
18
25
  const contents = `
19
- const route = [];
26
+ import router from "primate/router";
27
+ ${route_files.map((file, i) => {
28
+ const path = app.basename(file, app.path.routes);
29
+ return `import route${i} from "app:route/${path}";`;
30
+ }).join("\n")}
31
+ const routes = [];
20
32
  ${route_files.map((file, i) => {
21
33
  const path = app.basename(file, app.path.routes);
22
- return `const route${i} = (await import("app:route/${path}")).default;
23
- route.push(["${path}", route${i}]);`;
34
+ return is_hook_file(path)
35
+ ? `router.addHook("${path}", route${i});
36
+ routes.push(["${path}", route${i}]);
37
+ `
38
+ : `for (const [method, { handler, options }] of Object.entries(route${i})) {
39
+ router.add("${path}", method, handler, options);
40
+ }
41
+ routes.push(["${path}", route${i}]);`;
24
42
  }).join("\n")}
25
- export default route;
43
+ export default routes;
26
44
  `;
27
45
  const watchDirs = (await app.path.routes.dirs({
28
46
  recursive: true,
@@ -38,4 +56,5 @@ export default function plugin_server_virtual_routes(app) {
38
56
  },
39
57
  };
40
58
  }
59
+ export default plugin_server_virtual_routes;
41
60
  //# sourceMappingURL=virtual-routes.js.map
@@ -20,13 +20,14 @@ export default function plugin_server_wasm(app) {
20
20
  });
21
21
  build.onLoad({ filter: /.*/, namespace: "wasm-dev" }, async (args) => {
22
22
  const wasm_file = app.runpath("wasm", args.path + ".wasm");
23
+ const fs_path = import.meta.resolve("@rcompat/fs");
23
24
  return {
24
25
  contents: `
25
- import fs from "@rcompat/fs";
26
+ const { default: fs } = await import("${fs_path}");
26
27
  export default await fs.ref("${wasm_file.path}").bytes();
27
28
  `,
28
29
  loader: "js",
29
- resolveDir: app.root.path,
30
+ resolveDir: new URL(".", import.meta.url).pathname,
30
31
  };
31
32
  });
32
33
  },
@@ -4,7 +4,7 @@ import type { Dict } from "@rcompat/type";
4
4
  type ClientData<T extends Dict = Dict> = {
5
5
  view: string;
6
6
  request: RequestView;
7
- spa: boolean;
7
+ csr: boolean;
8
8
  ssr: boolean;
9
9
  mode: Mode;
10
10
  } & T;
@@ -2,7 +2,7 @@ import navigate from "#client/navigate";
2
2
  import root from "#client/root";
3
3
  import storage from "#client/storage";
4
4
  import submit from "#client/submit";
5
- import { MIME } from "@rcompat/http";
5
+ import http from "@rcompat/http";
6
6
  export default (u) => {
7
7
  root.set(u);
8
8
  const { location, history } = globalThis;
@@ -50,7 +50,7 @@ export default (u) => {
50
50
  const action = target.action ?? location.pathname;
51
51
  const url = new URL(action);
52
52
  const data = new FormData(target);
53
- const form = enctype === MIME.MULTIPART_FORM_DATA
53
+ const form = enctype === http.MIME.MULTIPART_FORM_DATA
54
54
  ? data
55
55
  : new URLSearchParams(data);
56
56
  try {
@@ -1,6 +1,6 @@
1
1
  import type { Dict } from "@rcompat/type";
2
2
  type FormId = string;
3
- type FieldErrors = readonly string[];
3
+ type FieldErrors = string[];
4
4
  type FormErrors = {
5
5
  form: FieldErrors;
6
6
  fields: Dict<FieldErrors>;
@@ -15,11 +15,21 @@ export type FormView = FormSnapshot & {
15
15
  submit: (event?: Event) => Promise<void>;
16
16
  };
17
17
  type FormSubscriber = (snapshot: FormSnapshot) => void;
18
+ export type MethodMeta = {
19
+ contentType?: string;
20
+ };
21
+ export type ClientMethod<Values extends Dict = Dict> = MethodMeta & ((args: {
22
+ body: Values;
23
+ }) => Promise<Response>);
18
24
  export type FormInit = {
19
25
  id?: string;
20
26
  method?: "POST" | "PUT" | "PATCH" | "DELETE";
21
27
  url?: string;
22
28
  headers?: Dict<string>;
29
+ action?: (args: {
30
+ body: unknown;
31
+ }) => Promise<Response>;
32
+ contentType?: string;
23
33
  };
24
34
  type FormController = {
25
35
  subscribe(fn: FormSubscriber): () => void;
@@ -1,5 +1,6 @@
1
1
  import extract_issues from "#client/extract-issues";
2
2
  import submit from "#client/submit";
3
+ import is from "@rcompat/is";
3
4
  function decode_pointer_segment(segment) {
4
5
  // decode JSON Pointer: ~1 -> /, ~0 -> ~
5
6
  return segment.replace(/~1/g, "/").replace(/~0/g, "~");
@@ -21,6 +22,21 @@ function pointer_to_fieldname(path) {
21
22
  }
22
23
  return name;
23
24
  }
25
+ function content_type_body(contentType, form_data) {
26
+ if (contentType === "application/json")
27
+ return Object.fromEntries(form_data);
28
+ if (contentType === "multipart/form-data")
29
+ return form_data;
30
+ // default: application/x-www-form-urlencoded
31
+ return new URLSearchParams(form_data);
32
+ }
33
+ function form_data_body(form_data) {
34
+ for (const value of form_data.values()) {
35
+ if (value instanceof File && value.size > 0)
36
+ return form_data;
37
+ }
38
+ return new URLSearchParams(form_data);
39
+ }
24
40
  export default function createForm(init) {
25
41
  const id = init.id ?? `form-${crypto.randomUUID()}`;
26
42
  let snapshot = {
@@ -83,7 +99,9 @@ export default function createForm(init) {
83
99
  const form_data = new FormData(form_element);
84
100
  setSubmitting(true);
85
101
  try {
86
- const response = await submit(url, form_data, method);
102
+ const response = await (is.defined(init.action)
103
+ ? init.action({ body: content_type_body(init.contentType, form_data) })
104
+ : submit(url, form_data_body(form_data), method));
87
105
  if (response.ok) {
88
106
  // on success: clear errors, let the app decide what to do next
89
107
  // (redirect/reload)
@@ -91,8 +109,8 @@ export default function createForm(init) {
91
109
  publish();
92
110
  return;
93
111
  }
94
- const payload = await response.json();
95
- const issues = extract_issues(payload); // all issues, all paths
112
+ // all issues, all paths
113
+ const issues = extract_issues(await response.json());
96
114
  setErrors(issues);
97
115
  }
98
116
  catch (error) {
@@ -1,4 +1,4 @@
1
- import type { FormInit, FormView } from "#client/create-form";
1
+ import type { ClientMethod, FormInit, FormView, MethodMeta } from "#client/create-form";
2
2
  import createForm from "#client/create-form";
3
3
  import type Data from "#client/Data";
4
4
  import type Publish from "#client/Publish";
@@ -19,5 +19,5 @@ declare const client: {
19
19
  toValidated: typeof toValidated;
20
20
  };
21
21
  export default client;
22
- export type { Data, FormInit, FormView, Publish, Render, ValidateInit, ValidateUpdater, ValidationError, ViewResponse, };
22
+ export type { ClientMethod, Data, FormInit, FormView, MethodMeta, Publish, Render, ValidateInit, ValidateUpdater, ValidationError, ViewResponse };
23
23
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,7 @@
1
1
  type Location = {
2
2
  hash: string;
3
3
  pathname: string;
4
+ search: string;
4
5
  };
5
6
  declare function goto(target: Location, state?: boolean, after?: () => void): Promise<void>;
6
7
  declare function go(href: string, event?: Event): Promise<void>;
@@ -1,9 +1,9 @@
1
- import http from "#client/http";
2
1
  import root from "#client/root";
3
2
  import storage from "#client/storage";
4
- import { MIME } from "@rcompat/http";
3
+ import transport from "#client/transport";
4
+ import http from "@rcompat/http";
5
5
  const headers = {
6
- Accept: MIME.APPLICATION_JSON,
6
+ Accept: http.MIME.APPLICATION_JSON,
7
7
  };
8
8
  const get_by_id_or_name = (name) => document.getElementById(name) ?? document.getElementsByName(name)[0];
9
9
  const scroll_hash = (hash) => {
@@ -18,13 +18,14 @@ async function goto(target, state = false, after) {
18
18
  try {
19
19
  const { scrollTop } = globalThis.document.scrollingElement;
20
20
  const { location } = globalThis;
21
- const { requested, response } = await http.refetch(target.pathname, { headers });
22
- if (http.is_json(response)) {
21
+ const path = target.pathname + target.search;
22
+ const { requested, response } = await transport.refetch(path, { headers });
23
+ if (transport.is_json(response)) {
23
24
  if (response.ok)
24
25
  root.update(await response.json());
25
26
  if (state) {
26
27
  storage.new({ hash: location.hash, pathname: location.pathname, scrollTop });
27
- history.pushState({}, "", `${target.pathname}${target.hash}`);
28
+ history.pushState({}, "", `${path}${target.hash}`);
28
29
  }
29
30
  after?.();
30
31
  return;
@@ -1,2 +1,3 @@
1
- export default function submit(pathname: string, body: any, method: string): Promise<Response>;
1
+ declare function submit(pathname: string, body: any, method: string): Promise<Response>;
2
+ export default submit;
2
3
  //# sourceMappingURL=submit.d.ts.map
@@ -1,18 +1,18 @@
1
- import http from "#client/http";
2
1
  import root from "#client/root";
3
2
  import storage from "#client/storage";
4
- import { MIME } from "@rcompat/http";
3
+ import transport from "#client/transport";
4
+ import http from "@rcompat/http";
5
5
  const headers = {
6
- Accept: MIME.APPLICATION_JSON,
6
+ Accept: http.MIME.APPLICATION_JSON,
7
7
  };
8
- export default async function submit(pathname, body, method) {
9
- const { requested, response } = await http.refetch(pathname, {
8
+ async function submit(pathname, body, method) {
9
+ const { requested, response } = await transport.refetch(pathname, {
10
10
  body, headers, method,
11
11
  });
12
12
  if (response.redirected) {
13
13
  const { location, document, history } = globalThis;
14
14
  const scrollTop = document.scrollingElement?.scrollTop ?? 0;
15
- if (http.is_json(response))
15
+ if (transport.is_json(response))
16
16
  root.update(await response.json());
17
17
  storage.new({
18
18
  hash: location.hash,
@@ -23,7 +23,7 @@ export default async function submit(pathname, body, method) {
23
23
  history.pushState({}, "", url.pathname + url.search);
24
24
  return response;
25
25
  }
26
- if (http.is_json(response)) {
26
+ if (transport.is_json(response)) {
27
27
  if (response.ok) {
28
28
  root.update(await response.json());
29
29
  history.replaceState({}, "", requested.pathname + requested.search);
@@ -38,4 +38,5 @@ export default async function submit(pathname, body, method) {
38
38
  globalThis.location.assign(target);
39
39
  return response;
40
40
  }
41
+ export default submit;
41
42
  //# sourceMappingURL=submit.js.map
@@ -4,10 +4,10 @@ declare function refetch(input: string | URL, init?: RequestInit, max_hops?: num
4
4
  }>;
5
5
  declare function is_json(response: Response): boolean;
6
6
  declare function submit(pathname: string, body: any, method: string): Promise<Response>;
7
- declare const http: {
7
+ declare const transport: {
8
8
  refetch: typeof refetch;
9
9
  is_json: typeof is_json;
10
10
  submit: typeof submit;
11
11
  };
12
- export default http;
13
- //# sourceMappingURL=http.d.ts.map
12
+ export default transport;
13
+ //# sourceMappingURL=transport.d.ts.map
@@ -1,4 +1,4 @@
1
- import { MIME } from "@rcompat/http";
1
+ import http from "@rcompat/http";
2
2
  const sameorigin = (url) => url.origin === globalThis.location.origin;
3
3
  const get_location = (response, base) => {
4
4
  if (response.type === "opaqueredirect")
@@ -7,6 +7,8 @@ const get_location = (response, base) => {
7
7
  return location !== null ? new URL(location, base) : null;
8
8
  };
9
9
  async function refetch(input, init = {}, max_hops = 5) {
10
+ console.log("location.href", globalThis.location.href);
11
+ console.log("input", input.toString());
10
12
  let url = new URL(input.toString(), globalThis.location.href);
11
13
  const method = (init.method ?? "GET").toUpperCase();
12
14
  let hops = 0;
@@ -36,7 +38,7 @@ async function refetch(input, init = {}, max_hops = 5) {
36
38
  }
37
39
  function is_json(response) {
38
40
  const raw = response.headers.get("content-type") ?? "";
39
- return raw.split(";")[0].trim() === MIME.APPLICATION_JSON;
41
+ return raw.split(";")[0].trim() === http.MIME.APPLICATION_JSON;
40
42
  }
41
43
  async function submit(pathname, body, method) {
42
44
  const { requested, response } = await refetch(pathname, { body, method });
@@ -48,10 +50,6 @@ async function submit(pathname, body, method) {
48
50
  }
49
51
  return response;
50
52
  }
51
- const http = {
52
- refetch,
53
- is_json,
54
- submit,
55
- };
56
- export default http;
57
- //# sourceMappingURL=http.js.map
53
+ const transport = { refetch, is_json, submit };
54
+ export default transport;
55
+ //# sourceMappingURL=transport.js.map
@@ -3,6 +3,7 @@ import type Module from "#Module";
3
3
  import type { Dict } from "@rcompat/type";
4
4
  import type { ObjectType, Parsed } from "pema";
5
5
  declare const _default: ObjectType<import("pema").NormalizeSchemaObject<{
6
+ readonly conditions: import("pema").DefaultType<import("pema").ArrayType<import("pema").StringType>, string[]>;
6
7
  readonly http: {
7
8
  readonly csp: import("pema").OptionalType<import("pema").RecordType<import("pema").StringType, import("pema").ArrayType<import("pema").StringType>>>;
8
9
  readonly headers: import("pema").OptionalType<import("pema").RecordType<import("pema").StringType, import("pema").StringType>>;
@@ -24,10 +25,10 @@ declare const _default: ObjectType<import("pema").NormalizeSchemaObject<{
24
25
  readonly db: {
25
26
  readonly migrations: import("pema").OptionalType<ObjectType<{
26
27
  table: import("pema").StringType;
27
- db: import("pema").PureType<DB, "PureType">;
28
+ db: import("pema").PureType<DB<unknown>, "PureType">;
28
29
  }, {
29
30
  table: string;
30
- db: DB;
31
+ db: DB<unknown>;
31
32
  }>>;
32
33
  };
33
34
  readonly env: {
@@ -39,12 +40,8 @@ declare const _default: ObjectType<import("pema").NormalizeSchemaObject<{
39
40
  name: import("pema").StringType;
40
41
  setup: import("pema").FunctionType;
41
42
  }, Module>>, Module[]>;
42
- readonly request: {
43
- readonly body: {
44
- readonly parse: import("pema").DefaultType<import("pema").BooleanType, true>;
45
- };
46
- };
47
43
  }>, {
44
+ conditions: string[];
48
45
  http: {
49
46
  csp: Record<string, string[]> | undefined;
50
47
  headers: Record<string, string> | undefined;
@@ -66,7 +63,7 @@ declare const _default: ObjectType<import("pema").NormalizeSchemaObject<{
66
63
  db: {
67
64
  migrations: {
68
65
  table: string;
69
- db: DB;
66
+ db: DB<unknown>;
70
67
  } | undefined;
71
68
  };
72
69
  env: {
@@ -75,11 +72,6 @@ declare const _default: ObjectType<import("pema").NormalizeSchemaObject<{
75
72
  }> | undefined;
76
73
  };
77
74
  modules: Module[];
78
- request: {
79
- body: {
80
- parse: boolean;
81
- };
82
- };
83
75
  }>;
84
76
  export default _default;
85
77
  //# sourceMappingURL=schema.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import fs from "@rcompat/fs";
2
2
  import p from "pema";
3
3
  export default p({
4
+ conditions: p.array(p.string).unique().default([]),
4
5
  http: {
5
6
  csp: p.dict(p.array(p.string)).optional(),
6
7
  headers: p.dict().optional(),
@@ -34,10 +35,5 @@ export default p({
34
35
  }).shape())
35
36
  .uniqueBy(member => member.name)
36
37
  .default([]),
37
- request: {
38
- body: {
39
- parse: p.boolean.default(true),
40
- },
41
- },
42
38
  });
43
39
  //# sourceMappingURL=schema.js.map
@@ -21,9 +21,10 @@ export type Schema = {
21
21
  introspect(name: string, pk?: PK): MaybePromise<MaybeTable>;
22
22
  alter(name: string, diff: SchemaDiff): MaybePromise<void>;
23
23
  };
24
- export default interface DB {
24
+ export default interface DB<Client = unknown> {
25
25
  schema: Schema;
26
26
  close(): MaybePromise<void>;
27
+ readonly client: Client;
27
28
  create<O extends Dict>(as: As, record: Dict): MaybePromise<O>;
28
29
  read(as: As, args: {
29
30
  count: true;
@@ -34,6 +35,7 @@ export default interface DB {
34
35
  where: Dict;
35
36
  fields?: string[];
36
37
  limit?: number;
38
+ offset?: number;
37
39
  sort?: Sort;
38
40
  with?: With;
39
41
  }): MaybePromise<Dict[]>;
@@ -4,9 +4,11 @@ import type { Schema } from "#db/DB";
4
4
  import type DataDict from "#db/DataDict";
5
5
  import type Sort from "#db/Sort";
6
6
  import type With from "#db/With";
7
- import type { Dict, MaybePromise } from "@rcompat/type";
8
- export default class MemoryDB implements DB {
7
+ import type { Dict, MaybePromise, PartialDict } from "@rcompat/type";
8
+ type Tables = PartialDict<Dict[]>;
9
+ export default class MemoryDB implements DB<Tables> {
9
10
  #private;
11
+ get client(): Tables;
10
12
  get schema(): Schema;
11
13
  close(): void;
12
14
  create<O extends Dict>(as: As, record: Dict): MaybePromise<O>;
@@ -30,4 +32,5 @@ export default class MemoryDB implements DB {
30
32
  where: DataDict;
31
33
  }): number;
32
34
  }
35
+ export {};
33
36
  //# sourceMappingURL=MemoryDB.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import E from "#db/errors";
2
2
  import assert from "@rcompat/assert";
3
- import entries from "@rcompat/dict/entries";
3
+ import dict from "@rcompat/dict";
4
4
  import is from "@rcompat/is";
5
5
  function escape_re(s) {
6
6
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -83,6 +83,22 @@ function match(record, where) {
83
83
  return false;
84
84
  break;
85
85
  }
86
+ case "$in": {
87
+ const arr = op_val;
88
+ if (is.date(value)) {
89
+ if (!arr.some(v => is.date(v) && v.getTime() === value.getTime()))
90
+ return false;
91
+ break;
92
+ }
93
+ if (is.url(value)) {
94
+ if (!arr.some(v => is.url(v) && v.href === value.href))
95
+ return false;
96
+ break;
97
+ }
98
+ if (!arr.includes(value))
99
+ return false;
100
+ break;
101
+ }
86
102
  default:
87
103
  throw E.operator_unknown(k, op);
88
104
  }
@@ -101,24 +117,20 @@ function filter(record, fields) {
101
117
  return Object.fromEntries(Object.entries(record)
102
118
  .filter(([key]) => fields.includes(key)));
103
119
  }
104
- function toSorted(d1, d2, sort) {
105
- return [...entries(sort).valmap(([, value]) => value === "asc" ? 1 : -1)]
120
+ function to_sorted(d1, d2, sort) {
121
+ return (dict.entries(sort)
122
+ .map(([k, value]) => [k, value === "asc" ? 1 : -1])
106
123
  .reduce((sorting, [field, direction]) => {
107
124
  const left = d1[field];
108
125
  const right = d2[field];
109
- // if sorting has been established, it stays fixed
110
- if (sorting !== 0) {
126
+ if (sorting !== 0)
111
127
  return sorting;
112
- }
113
- // equal, sorting doesn't change
114
- if (left === right) {
128
+ if (left === right)
115
129
  return sorting;
116
- }
117
- if (left < right) {
130
+ if (left < right)
118
131
  return -1 * direction;
119
- }
120
132
  return direction;
121
- }, 0);
133
+ }, 0));
122
134
  }
123
135
  export default class MemoryDB {
124
136
  #tables = {};
@@ -127,6 +139,9 @@ export default class MemoryDB {
127
139
  this.#tables[name] ??= [];
128
140
  return this.#tables[name];
129
141
  }
142
+ get client() {
143
+ return this.#tables;
144
+ }
130
145
  get schema() {
131
146
  return {
132
147
  create: async (table, _pk, types) => {
@@ -206,9 +221,10 @@ export default class MemoryDB {
206
221
  const sort = args.sort ?? {};
207
222
  const sorted = Object.keys(sort).length === 0
208
223
  ? matches
209
- : matches.toSorted((a, b) => toSorted(a, b, sort));
224
+ : matches.toSorted((a, b) => to_sorted(a, b, sort));
225
+ const offset = args.offset ?? 0;
210
226
  const limit = args.limit ?? sorted.length;
211
- const base_rows = sorted.slice(0, limit);
227
+ const base_rows = sorted.slice(offset, offset + limit);
212
228
  // no relations -> preserve existing behavior
213
229
  if (args.with === undefined) {
214
230
  const fields = args.fields ?? [];
@@ -258,7 +274,7 @@ export default class MemoryDB {
258
274
  // filter by target_pk equality + optional where
259
275
  let candidates = target.filter(t => t[target_pk] === fk_value && match(t, r_where));
260
276
  if (r_sort !== undefined && Object.keys(r_sort).length > 0) {
261
- candidates = candidates.toSorted((a, b) => toSorted(a, b, r_sort));
277
+ candidates = candidates.toSorted((a, b) => to_sorted(a, b, r_sort));
262
278
  }
263
279
  if (r_limit !== undefined)
264
280
  candidates = candidates.slice(0, r_limit);
@@ -290,7 +306,7 @@ export default class MemoryDB {
290
306
  const key = parent_full[parent_pk];
291
307
  let rows = grouped.get(key) ?? [];
292
308
  if (r_sort !== undefined && Object.keys(r_sort).length > 0) {
293
- rows = rows.toSorted((a, b) => toSorted(a, b, r_sort));
309
+ rows = rows.toSorted((a, b) => to_sorted(a, b, r_sort));
294
310
  }
295
311
  if (kind === "one") {
296
312
  if (r_limit !== undefined)
@@ -314,9 +330,7 @@ export default class MemoryDB {
314
330
  const matched = table.filter(record => match(record, args.where));
315
331
  const pk = as.pk;
316
332
  return matched.map(record => {
317
- const changed = entries({ ...record, ...args.set })
318
- .filter(([, value]) => value !== null)
319
- .get();
333
+ const changed = dict.filter({ ...record, ...args.set }, (_, value) => !is.null(value));
320
334
  const index = pk !== null
321
335
  ? table.findIndex(stored => stored[pk] === record[pk])
322
336
  : table.findIndex(stored => stored === record);
@@ -0,0 +1,23 @@
1
+ import type DB from "#db/DB";
2
+ import type { ArrayType, ObjectType, OptionalType, Parsed } from "pema";
3
+ export type SQLInput = ObjectType<any>;
4
+ export type SQLOutput = ArrayType<any>;
5
+ export type WordChar = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "_";
6
+ export type ReadWord<S extends string, Acc extends string = ""> = S extends `${infer C extends WordChar}${infer Rest}` ? ReadWord<Rest, `${Acc}${C}`> : Acc;
7
+ export type ExtractPlaceholders<S extends string> = S extends `${string}:${infer After}` ? ReadWord<After> | ExtractPlaceholders<After> : never;
8
+ type RequiredKeys<I extends ObjectType<any>> = {
9
+ [K in keyof I["properties"]]: I["properties"][K] extends OptionalType<any> ? never : K;
10
+ }[keyof I["properties"]];
11
+ type MissingPlaceholders<I extends ObjectType<any>, Q extends string> = Exclude<RequiredKeys<I>, ExtractPlaceholders<Q>>;
12
+ export type CheckPlaceholders<I extends ObjectType<any>, Q extends string> = MissingPlaceholders<I, Q> extends never ? I : `Missing placeholders in query: ${MissingPlaceholders<I, Q> & string}`;
13
+ export default interface SQLDB<Client = unknown> extends DB<Client> {
14
+ sql<Q extends string, I extends ObjectType<{
15
+ [K in ExtractPlaceholders<Q>]: Parsed<unknown>;
16
+ }> | undefined, O extends Parsed<unknown> | undefined>(options: {
17
+ input?: I extends ObjectType<any> ? CheckPlaceholders<I, Q> : I;
18
+ query: Q;
19
+ output?: O;
20
+ }): (args: I extends ObjectType<any> ? I["infer"] : void) => Promise<O extends Parsed<unknown> ? O["infer"] : void>;
21
+ }
22
+ export {};
23
+ //# sourceMappingURL=SQLDB.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=SQLDB.js.map