evikit 2.1.0 → 2.2.1

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 CHANGED
@@ -52,7 +52,7 @@ cd mysite
52
52
  npm init -y
53
53
 
54
54
  # Routing and validation.
55
- npm i accept-language classnames evikit express hoofd preact preact-iso valibot
55
+ npm i accept-language classnames evikit express file-type hoofd preact preact-iso valibot
56
56
  npm i -D @preact/preset-vite cross-env vite vite-bundle-analyzer
57
57
 
58
58
  # API documentation.
@@ -85,7 +85,7 @@ node_modules
85
85
  Let's start by modelling a relational database schema. We have a question with multiple text choice options. Create `src/lib/db/schema.js`:
86
86
 
87
87
  ```js
88
- import { primary, references } from "evikit/db";
88
+ import { primary, references } from "evikit";
89
89
  import { integer, number, object, optional, pipe, string } from "valibot";
90
90
 
91
91
  export const Question = object({
@@ -688,6 +688,158 @@ form(
688
688
  ),
689
689
  ```
690
690
 
691
+ ### File upload
692
+
693
+ Why not let each choice have an icon?
694
+
695
+ In `src/lib/db/schema.js`:
696
+
697
+ ```js
698
+ import { instance } from "valibot";
699
+
700
+ // Other imports and Question schema...
701
+
702
+ export const Storage = object({
703
+ content: instance(Uint8Array),
704
+ id: pipe(string(), primary()),
705
+ });
706
+
707
+ export const Choice = object({
708
+ iconStorageId: pipe(string(), references(Storage, "id")),
709
+ // The rest of the Choice schema...
710
+ });
711
+ ```
712
+
713
+ In `src/routes/api/choice/types.js`:
714
+
715
+ ```js
716
+ import { file } from "valibot";
717
+ // Other imports...
718
+
719
+ export const ChoicePost = {
720
+ input: object({
721
+ icon: file(),
722
+ // The rest of the schema...
723
+ ```
724
+
725
+ In `src/routes/api/choice/server.js`:
726
+
727
+ ```js
728
+ import { fileTypeFromBuffer } from "file-type";
729
+ import { createHash } from "node:crypto";
730
+ import { parse, picklist } from "valibot";
731
+
732
+ // ...
733
+ if (!question) {
734
+ return text(i18n.gettext("Question does not exist"), 400);
735
+ }
736
+
737
+ const buffer = Buffer.from(await input.icon.arrayBuffer());
738
+
739
+ // Fail if user sent something else than an image.
740
+ const fileType = await fileTypeFromBuffer(buffer);
741
+ ok(fileType);
742
+ const { ext } = fileType;
743
+ parse(picklist(["gif", "jpg", "png", "webp"]), ext);
744
+
745
+ // Don't save another image if hash already known.
746
+ const iconStorageId = `${createHash("sha256").update(buffer).digest("hex")}.${ext}`;
747
+ db.Storage.upsert({
748
+ content: new Uint8Array(buffer.buffer),
749
+ id: iconStorageId,
750
+ });
751
+
752
+ const [choice] = db.Choice.upsert({
753
+ iconStorageId,
754
+ // ...
755
+ ```
756
+
757
+ In `src/routes/api/question/[id]/types.js`:
758
+
759
+ ```js
760
+ // ...
761
+ question: object({
762
+ ...schema.Question.entries,
763
+ Choice: array(
764
+ object({
765
+ ...schema.Choice.entries,
766
+ iconUrl: string(),
767
+ }),
768
+ ),
769
+ }),
770
+ // ...
771
+ ```
772
+
773
+ In `src/routes/api/question[id]/server.js`:
774
+
775
+ ```js
776
+ import { fileTypeFromBuffer } from "file-type";
777
+ import { ok } from "node:assert/strict";
778
+
779
+ // ...
780
+
781
+ const Question = db.Question.select({
782
+ with: { Choice: { with: { iconStorage: true } } },
783
+ });
784
+
785
+ // ...
786
+
787
+ return json({
788
+ question: {
789
+ ...question,
790
+ Choice: await Promise.all(
791
+ question.Choice.map(async (choice) => {
792
+ ok(choice.iconStorage);
793
+ const buffer = Buffer.from(choice.iconStorage.content.buffer);
794
+ const fileType = await fileTypeFromBuffer(buffer);
795
+ ok(fileType);
796
+ return {
797
+ ...choice,
798
+ iconUrl: `data:${fileType.mime};base64,${buffer.toString("base64")}`,
799
+ };
800
+ }),
801
+ ),
802
+ },
803
+ });
804
+ ```
805
+
806
+ In `src/routes/question/[id]/page.js`:
807
+
808
+ ```js
809
+ import { img } from "evikit";
810
+ // ...
811
+
812
+ form(
813
+ {
814
+ action: "/api/choice",
815
+ enctype: "multipart/form-data",
816
+ method: "POST",
817
+ onSubmit: suggestPost.onSubmit,
818
+ },
819
+ // ...
820
+ p(
821
+ { class: "fieldset" },
822
+ label({ for: "icon" }, i18n.gettext("Icon")),
823
+ input({ id: "icon", name: "icon", type: "file" }),
824
+ ),
825
+ // The rest of the /api/choice form...
826
+ ),
827
+
828
+ form(
829
+ { action: "/api/vote", method: "POST", onSubmit: votePost.onSubmit },
830
+ // ...
831
+ input({
832
+ id: choice.id,
833
+ name: "choiceId",
834
+ type: "radio",
835
+ value: choice.id,
836
+ }),
837
+ label(
838
+ { for: choice.id },
839
+ img({ alt: "", height: "24", src: choice.iconUrl, width: "24" }),
840
+ // ...
841
+ ```
842
+
691
843
  ### Translation
692
844
 
693
845
  For the Ukrainian language, which has the `uk` [two-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes), create an empty file `po/uk.po`. It will use the [gettext](https://www.gnu.org/software/gettext/) format, which is well-known to professional translators.
package/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import type { ClassAttributes, ComponentChildren, JSX, VNode } from "preact";
2
2
 
3
3
  import i18n from "gettext.js";
4
4
 
5
+ import { flatten, primary, references, relations } from "./db.js";
5
6
  import { useApi } from "./use-api.js";
6
7
  import { useEnhance } from "./use-enhance.js";
7
8
 
@@ -113,4 +114,5 @@ export const svg: H<"svg">;
113
114
  export const title: H<"title">;
114
115
  export const ul: H<"ul">;
115
116
 
117
+ export { flatten, primary, references, relations };
116
118
  export { useApi, useEnhance };
package/index.js CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  import { locationStub } from "preact-iso/prerender";
11
11
  import { useState } from "preact/hooks";
12
12
 
13
+ import { flatten, primary, references, relations } from "./db.js";
13
14
  import {
14
15
  a,
15
16
  b,
@@ -171,6 +172,8 @@ export async function ssr(App, data) {
171
172
  };
172
173
  }
173
174
 
175
+ export { flatten, primary, references, relations };
176
+
174
177
  export { getI18n, useApi, useEnhance, useI18n, useQueryClient, useStatus };
175
178
 
176
179
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evikit",
3
3
  "description": "Preact/SQLite SSR framework for small web apps",
4
- "version": "2.1.0",
4
+ "version": "2.2.1",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./index.js",
package/sqlite.js CHANGED
@@ -71,7 +71,10 @@ export function sqlite({ pkg, schema }) {
71
71
  : entry.expects == "number" ||
72
72
  entry.expects == "(number | undefined)"
73
73
  ? "INTEGER"
74
- : fail(name),
74
+ : entry.expects == "Uint8Array" ||
75
+ entry.expects == "(Uint8Array | undefined)"
76
+ ? "BLOB"
77
+ : fail(name),
75
78
  };
76
79
  }),
77
80
  foreignKeys: Object.entries(entries)