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 +154 -2
- package/index.d.ts +2 -0
- package/index.js +3 -0
- package/package.json +1 -1
- package/sqlite.js +4 -1
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
|
|
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
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
|
-
:
|
|
74
|
+
: entry.expects == "Uint8Array" ||
|
|
75
|
+
entry.expects == "(Uint8Array | undefined)"
|
|
76
|
+
? "BLOB"
|
|
77
|
+
: fail(name),
|
|
75
78
|
};
|
|
76
79
|
}),
|
|
77
80
|
foreignKeys: Object.entries(entries)
|