("");
12 | return (
13 |
36 | );
37 | }
38 |
39 | const TodoView = memo(
40 | ({
41 | todo,
42 | editing,
43 | startEditing,
44 | saveTodo,
45 | }: {
46 | key?: any;
47 | todo: Todo;
48 | editing: boolean;
49 | startEditing: (t: Todo) => void;
50 | saveTodo: (todo: Todo, text: string) => void;
51 | }) => {
52 | let body;
53 |
54 | const [text, setText] = useState(todo.text);
55 | useBind(todo, ["text", "completed"]);
56 | const deleteTodo = () => todo.delete().save();
57 | const toggleTodo = () => todo.update({ completed: !todo.completed }).save();
58 |
59 | if (editing) {
60 | body = (
61 | saveTodo(todo, text)}
67 | onKeyUp={(e) => e.key === "Enter" && saveTodo(todo, text)}
68 | onChange={(e) => setText(e.target.value)}
69 | />
70 | );
71 | } else {
72 | body = (
73 |
74 |
80 |
81 |
82 |
83 | );
84 | }
85 | return (
86 |
91 | {body}
92 |
93 | );
94 | }
95 | );
96 |
97 | function Footer({
98 | remaining,
99 | todos,
100 | clearCompleted,
101 | todoList,
102 | }: {
103 | remaining: number;
104 | todos: Todo[];
105 | clearCompleted: () => void;
106 | todoList: TodoList;
107 | }) {
108 | let clearCompletedButton;
109 | if (remaining !== todos.length) {
110 | clearCompletedButton = (
111 |
114 | );
115 | }
116 |
117 | const updateFilter = (filter: Filter) => todoList.update({ filter }).save();
118 |
119 | return (
120 |
154 | );
155 | }
156 |
157 | export default function App({ list }: { list: TodoList }) {
158 | const clearCompleted = () =>
159 | commit(
160 | list.ctx,
161 | completeTodos.map((t) => t.delete())
162 | );
163 | const startEditing = useCallback(
164 | (todo: Todo) => list.update({ editing: todo.id }).save(),
165 | [list]
166 | );
167 | const saveTodo = useCallback(
168 | (todo: Todo, text: string) => {
169 | commit(
170 | list.ctx,
171 | todo.update({ text: text }),
172 | list.update({ editing: null })
173 | );
174 | },
175 | [list]
176 | );
177 | const toggleAll = () => {
178 | if (remaining === 0) {
179 | // uncomplete all
180 | commit(
181 | list.ctx,
182 | completeTodos.map((t) => t.update({ completed: false }))
183 | );
184 | } else {
185 | // complete all
186 | commit(
187 | list.ctx,
188 | activeTodos.map((t) => t.update({ completed: true }))
189 | );
190 | }
191 | };
192 | let toggleAllCheck;
193 |
194 | useBind(list, ["filter", "editing"]);
195 | const activeTodos = useQuery(() =>
196 | list.queryTodos().whereCompleted(P.equals(false))
197 | ).data;
198 | const completeTodos = useQuery(() =>
199 | list.queryTodos().whereCompleted(P.equals(true))
200 | ).data;
201 | const allTodos = useQuery(() => list.queryTodos(), [], {
202 | on: UpdateType.CREATE_OR_DELETE,
203 | }).data;
204 |
205 | const remaining = activeTodos.length;
206 | let todos =
207 | list.filter === "active"
208 | ? activeTodos
209 | : list.filter === "completed"
210 | ? completeTodos
211 | : allTodos;
212 |
213 | if (allTodos.length) {
214 | toggleAllCheck = (
215 | <>
216 |
223 |
224 | >
225 | );
226 | }
227 |
228 | return (
229 |
230 |
231 |
0 ? {} : { display: "none" }}
234 | >
235 | {toggleAllCheck}
236 |
237 | {todos.map((t) => (
238 |
245 | ))}
246 |
247 |
253 |
254 |
255 | );
256 | }
257 |
--------------------------------------------------------------------------------
/src/domain.aphro:
--------------------------------------------------------------------------------
1 | engine: sqlite
2 | db: todomvc
3 |
4 | Todo as Node {
5 | 1 id: ID
6 | 2 listId: ID
7 | 3 text: string
8 | 4 completed: bool
9 | }
10 |
11 | TodoList as Node {
12 | 1 id: ID
13 | 2 filter: Enumeration
14 | 3 editing: ID | null
15 | } & OutboundEdges {
16 | todos: Edge
17 | }
--------------------------------------------------------------------------------
/src/domain/Todo.ts:
--------------------------------------------------------------------------------
1 | import TodoBase from "./generated/TodoBase.js";
2 | export { Data } from "./generated/TodoBase.js";
3 |
4 | export default class Todo extends TodoBase {
5 | // insert any manual method you may have here
6 | }
7 |
--------------------------------------------------------------------------------
/src/domain/TodoList.ts:
--------------------------------------------------------------------------------
1 | import TodoListBase from "./generated/TodoListBase.js";
2 | export { Data } from "./generated/TodoListBase.js";
3 |
4 | export default class TodoList extends TodoListBase {
5 | // insert any manual method you may have here
6 | }
7 |
--------------------------------------------------------------------------------
/src/domain/generated/Todo.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- SIGNED-SOURCE: <05c4d9565ec657beb4af11557ef31bd4>
2 | -- STATEMENT
3 | CREATE TABLE
4 | "todo" (
5 | "id"
6 | /* n=1 */
7 | ,
8 | "listId"
9 | /* n=2 */
10 | ,
11 | "text"
12 | /* n=3 */
13 | ,
14 | "completed"
15 | /* n=4 */
16 | ,
17 | PRIMARY KEY ("id")
18 | );
--------------------------------------------------------------------------------
/src/domain/generated/TodoBase.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <6ca69fc65a5d5bc158e4a26e2056b046>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import Todo from "../Todo.js";
7 | import { default as s } from "./TodoSpec.js";
8 | import { P } from "@aphro/runtime-ts";
9 | import { UpdateMutationBuilder } from "@aphro/runtime-ts";
10 | import { CreateMutationBuilder } from "@aphro/runtime-ts";
11 | import { DeleteMutationBuilder } from "@aphro/runtime-ts";
12 | import { makeSavable } from "@aphro/runtime-ts";
13 | import { modelGenMemo } from "@aphro/runtime-ts";
14 | import { Node } from "@aphro/runtime-ts";
15 | import { NodeSpecWithCreate } from "@aphro/runtime-ts";
16 | import { SID_of } from "@aphro/runtime-ts";
17 | import TodoQuery from "./TodoQuery.js";
18 | import { Context } from "@aphro/runtime-ts";
19 | import TodoList from "../TodoList.js";
20 |
21 | export type Data = {
22 | id: SID_of;
23 | listId: SID_of;
24 | text: string;
25 | completed: boolean;
26 | };
27 |
28 | // @Sealed(Todo)
29 | export default abstract class TodoBase extends Node {
30 | readonly spec = s as unknown as NodeSpecWithCreate;
31 |
32 | get id(): SID_of {
33 | return this.data.id as unknown as SID_of;
34 | }
35 |
36 | get listId(): SID_of {
37 | return this.data.listId;
38 | }
39 |
40 | get text(): string {
41 | return this.data.text;
42 | }
43 |
44 | get completed(): boolean {
45 | return this.data.completed;
46 | }
47 |
48 | static queryAll(ctx: Context): TodoQuery {
49 | return TodoQuery.create(ctx);
50 | }
51 |
52 | static genx = modelGenMemo(
53 | "todomvc",
54 | "todo",
55 | (ctx: Context, id: SID_of): Promise =>
56 | this.queryAll(ctx).whereId(P.equals(id)).genxOnlyValue()
57 | );
58 |
59 | static gen = modelGenMemo(
60 | "todomvc",
61 | "todo",
62 | // @ts-ignore #43
63 | (ctx: Context, id: SID_of): Promise =>
64 | this.queryAll(ctx).whereId(P.equals(id)).genOnlyValue()
65 | );
66 |
67 | update(data: Partial) {
68 | return makeSavable(
69 | this.ctx,
70 | new UpdateMutationBuilder(this.ctx, this.spec, this)
71 | .set(data)
72 | .toChangesets()[0]
73 | );
74 | }
75 |
76 | static create(ctx: Context, data: Partial) {
77 | return makeSavable(
78 | ctx,
79 | new CreateMutationBuilder(ctx, s).set(data).toChangesets()[0]
80 | );
81 | }
82 |
83 | delete() {
84 | return makeSavable(
85 | this.ctx,
86 | new DeleteMutationBuilder(this.ctx, this.spec, this).toChangesets()[0]
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/domain/generated/TodoList.sqlite.sql:
--------------------------------------------------------------------------------
1 | -- SIGNED-SOURCE: <7048e89aae20f31a325983182d690969>
2 | -- STATEMENT
3 | CREATE TABLE
4 | "todolist" (
5 | "id"
6 | /* n=1 */
7 | ,
8 | "filter"
9 | /* n=2 */
10 | ,
11 | "editing"
12 | /* n=3 */
13 | ,
14 | PRIMARY KEY ("id")
15 | );
--------------------------------------------------------------------------------
/src/domain/generated/TodoListBase.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <2eeeabf58f0df27ad78fa7a0fc9163f0>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import TodoList from "../TodoList.js";
7 | import { default as s } from "./TodoListSpec.js";
8 | import { P } from "@aphro/runtime-ts";
9 | import { UpdateMutationBuilder } from "@aphro/runtime-ts";
10 | import { CreateMutationBuilder } from "@aphro/runtime-ts";
11 | import { DeleteMutationBuilder } from "@aphro/runtime-ts";
12 | import { makeSavable } from "@aphro/runtime-ts";
13 | import { modelGenMemo } from "@aphro/runtime-ts";
14 | import { Node } from "@aphro/runtime-ts";
15 | import { NodeSpecWithCreate } from "@aphro/runtime-ts";
16 | import { SID_of } from "@aphro/runtime-ts";
17 | import TodoListQuery from "./TodoListQuery.js";
18 | import { Context } from "@aphro/runtime-ts";
19 | import TodoQuery from "./TodoQuery.js";
20 | import Todo from "../Todo.js";
21 |
22 | export type Data = {
23 | id: SID_of;
24 | filter: "all" | "active" | "completed";
25 | editing: SID_of | null;
26 | };
27 |
28 | // @Sealed(TodoList)
29 | export default abstract class TodoListBase extends Node {
30 | readonly spec = s as unknown as NodeSpecWithCreate;
31 |
32 | get id(): SID_of {
33 | return this.data.id as unknown as SID_of;
34 | }
35 |
36 | get filter(): "all" | "active" | "completed" {
37 | return this.data.filter;
38 | }
39 |
40 | get editing(): SID_of | null {
41 | return this.data.editing;
42 | }
43 |
44 | queryTodos(): TodoQuery {
45 | return TodoQuery.create(this.ctx).whereListId(P.equals(this.id as any));
46 | }
47 |
48 | static queryAll(ctx: Context): TodoListQuery {
49 | return TodoListQuery.create(ctx);
50 | }
51 |
52 | static genx = modelGenMemo(
53 | "todomvc",
54 | "todolist",
55 | (ctx: Context, id: SID_of): Promise =>
56 | this.queryAll(ctx).whereId(P.equals(id)).genxOnlyValue()
57 | );
58 |
59 | static gen = modelGenMemo(
60 | "todomvc",
61 | "todolist",
62 | // @ts-ignore #43
63 | (ctx: Context, id: SID_of): Promise =>
64 | this.queryAll(ctx).whereId(P.equals(id)).genOnlyValue()
65 | );
66 |
67 | update(data: Partial) {
68 | return makeSavable(
69 | this.ctx,
70 | new UpdateMutationBuilder(this.ctx, this.spec, this)
71 | .set(data)
72 | .toChangesets()[0]
73 | );
74 | }
75 |
76 | static create(ctx: Context, data: Partial) {
77 | return makeSavable(
78 | ctx,
79 | new CreateMutationBuilder(ctx, s).set(data).toChangesets()[0]
80 | );
81 | }
82 |
83 | delete() {
84 | return makeSavable(
85 | this.ctx,
86 | new DeleteMutationBuilder(this.ctx, this.spec, this).toChangesets()[0]
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/domain/generated/TodoListQuery.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <9d0961e16b687321e8111ffbab374128>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import { Context } from "@aphro/runtime-ts";
7 | import { DerivedQuery } from "@aphro/runtime-ts";
8 | import { QueryFactory } from "@aphro/runtime-ts";
9 | import { modelLoad } from "@aphro/runtime-ts";
10 | import { filter } from "@aphro/runtime-ts";
11 | import { Predicate } from "@aphro/runtime-ts";
12 | import { take } from "@aphro/runtime-ts";
13 | import { orderBy } from "@aphro/runtime-ts";
14 | import { P } from "@aphro/runtime-ts";
15 | import { ModelFieldGetter } from "@aphro/runtime-ts";
16 | import { Expression } from "@aphro/runtime-ts";
17 | import { EmptyQuery } from "@aphro/runtime-ts";
18 | import { SID_of } from "@aphro/runtime-ts";
19 | import TodoList from "../TodoList.js";
20 | import { Data } from "./TodoListBase.js";
21 | import TodoListSpec from "./TodoListSpec.js";
22 | import Todo from "../Todo.js";
23 | import TodoSpec from "./TodoSpec.js";
24 | import TodoQuery from "./TodoQuery.js";
25 |
26 | export default class TodoListQuery extends DerivedQuery {
27 | static create(ctx: Context) {
28 | return new TodoListQuery(
29 | ctx,
30 | QueryFactory.createSourceQueryFor(ctx, TodoListSpec),
31 | modelLoad(ctx, TodoListSpec.createFrom)
32 | );
33 | }
34 |
35 | static empty(ctx: Context) {
36 | return new TodoListQuery(ctx, new EmptyQuery(ctx));
37 | }
38 |
39 | protected derive(expression: Expression): TodoListQuery {
40 | return new TodoListQuery(this.ctx, this, expression);
41 | }
42 |
43 | static fromId(ctx: Context, id: SID_of) {
44 | return this.create(ctx).whereId(P.equals(id));
45 | }
46 |
47 | whereId(p: Predicate) {
48 | return this.derive(
49 | // @ts-ignore #43
50 | filter(new ModelFieldGetter<"id", Data, TodoList>("id"), p)
51 | );
52 | }
53 |
54 | whereFilter(p: Predicate) {
55 | return this.derive(
56 | // @ts-ignore #43
57 | filter(new ModelFieldGetter<"filter", Data, TodoList>("filter"), p)
58 | );
59 | }
60 |
61 | whereEditing(p: Predicate) {
62 | return this.derive(
63 | // @ts-ignore #43
64 | filter(new ModelFieldGetter<"editing", Data, TodoList>("editing"), p)
65 | );
66 | }
67 | queryTodos(): TodoQuery {
68 | return new TodoQuery(
69 | this.ctx,
70 | QueryFactory.createHopQueryFor(
71 | this.ctx,
72 | this,
73 | TodoListSpec.outboundEdges.todos
74 | ),
75 | modelLoad(this.ctx, TodoSpec.createFrom)
76 | );
77 | }
78 |
79 | take(n: number) {
80 | return new TodoListQuery(this.ctx, this, take(n));
81 | }
82 |
83 | orderById(direction: "asc" | "desc" = "asc") {
84 | return this.derive(
85 | orderBy(new ModelFieldGetter<"id", Data, TodoList>("id"), direction)
86 | );
87 | }
88 |
89 | orderByFilter(direction: "asc" | "desc" = "asc") {
90 | return this.derive(
91 | orderBy(
92 | new ModelFieldGetter<"filter", Data, TodoList>("filter"),
93 | direction
94 | )
95 | );
96 | }
97 |
98 | orderByEditing(direction: "asc" | "desc" = "asc") {
99 | return this.derive(
100 | orderBy(
101 | new ModelFieldGetter<"editing", Data, TodoList>("editing"),
102 | direction
103 | )
104 | );
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/domain/generated/TodoListSpec.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <787b1a69dcc3e0b185d6c3554f27826a>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import { Context } from "@aphro/runtime-ts";
7 | import { decodeModelData } from "@aphro/runtime-ts";
8 | import { encodeModelData } from "@aphro/runtime-ts";
9 | import { SID_of } from "@aphro/runtime-ts";
10 | import { NodeSpecWithCreate } from "@aphro/runtime-ts";
11 | import { default as TodoSpec } from "./TodoSpec.js";
12 | import TodoList from "../TodoList.js";
13 | import { Data } from "./TodoListBase.js";
14 |
15 | const fields = {
16 | id: {
17 | encoding: "none",
18 | },
19 | filter: {
20 | encoding: "none",
21 | },
22 | editing: {
23 | encoding: "none",
24 | },
25 | } as const;
26 | const TodoListSpec: NodeSpecWithCreate = {
27 | type: "node",
28 | createFrom(ctx: Context, data: Data, raw: boolean = true) {
29 | const existing = ctx.cache.get(data["id"], "todomvc", "todolist");
30 | if (existing) {
31 | return existing;
32 | }
33 | if (raw) data = decodeModelData(data, fields);
34 | const result = new TodoList(ctx, data);
35 | ctx.cache.set(data["id"], result, "todomvc", "todolist");
36 | return result;
37 | },
38 |
39 | primaryKey: "id",
40 |
41 | storage: {
42 | engine: "sqlite",
43 | db: "todomvc",
44 | type: "sql",
45 | tablish: "todolist",
46 | },
47 |
48 | fields,
49 |
50 | outboundEdges: {
51 | todos: {
52 | type: "foreignKey",
53 | sourceField: "id",
54 | destField: "listId",
55 | get source() {
56 | return TodoListSpec;
57 | },
58 | get dest() {
59 | return TodoSpec;
60 | },
61 | },
62 | },
63 | };
64 |
65 | export default TodoListSpec;
66 |
--------------------------------------------------------------------------------
/src/domain/generated/TodoQuery.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <09f55db7d7704140206e46aadbdb81d3>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import { Context } from "@aphro/runtime-ts";
7 | import { DerivedQuery } from "@aphro/runtime-ts";
8 | import { QueryFactory } from "@aphro/runtime-ts";
9 | import { modelLoad } from "@aphro/runtime-ts";
10 | import { filter } from "@aphro/runtime-ts";
11 | import { Predicate } from "@aphro/runtime-ts";
12 | import { take } from "@aphro/runtime-ts";
13 | import { orderBy } from "@aphro/runtime-ts";
14 | import { P } from "@aphro/runtime-ts";
15 | import { ModelFieldGetter } from "@aphro/runtime-ts";
16 | import { Expression } from "@aphro/runtime-ts";
17 | import { EmptyQuery } from "@aphro/runtime-ts";
18 | import { SID_of } from "@aphro/runtime-ts";
19 | import Todo from "../Todo.js";
20 | import { Data } from "./TodoBase.js";
21 | import TodoSpec from "./TodoSpec.js";
22 | import TodoList from "../TodoList.js";
23 |
24 | export default class TodoQuery extends DerivedQuery {
25 | static create(ctx: Context) {
26 | return new TodoQuery(
27 | ctx,
28 | QueryFactory.createSourceQueryFor(ctx, TodoSpec),
29 | modelLoad(ctx, TodoSpec.createFrom)
30 | );
31 | }
32 |
33 | static empty(ctx: Context) {
34 | return new TodoQuery(ctx, new EmptyQuery(ctx));
35 | }
36 |
37 | protected derive(expression: Expression): TodoQuery {
38 | return new TodoQuery(this.ctx, this, expression);
39 | }
40 |
41 | static fromId(ctx: Context, id: SID_of) {
42 | return this.create(ctx).whereId(P.equals(id));
43 | }
44 |
45 | whereId(p: Predicate) {
46 | return this.derive(
47 | // @ts-ignore #43
48 | filter(new ModelFieldGetter<"id", Data, Todo>("id"), p)
49 | );
50 | }
51 |
52 | whereListId(p: Predicate) {
53 | return this.derive(
54 | // @ts-ignore #43
55 | filter(new ModelFieldGetter<"listId", Data, Todo>("listId"), p)
56 | );
57 | }
58 |
59 | whereText(p: Predicate) {
60 | return this.derive(
61 | // @ts-ignore #43
62 | filter(new ModelFieldGetter<"text", Data, Todo>("text"), p)
63 | );
64 | }
65 |
66 | whereCompleted(p: Predicate) {
67 | return this.derive(
68 | // @ts-ignore #43
69 | filter(new ModelFieldGetter<"completed", Data, Todo>("completed"), p)
70 | );
71 | }
72 |
73 | take(n: number) {
74 | return new TodoQuery(this.ctx, this, take(n));
75 | }
76 |
77 | orderById(direction: "asc" | "desc" = "asc") {
78 | return this.derive(
79 | orderBy(new ModelFieldGetter<"id", Data, Todo>("id"), direction)
80 | );
81 | }
82 |
83 | orderByListId(direction: "asc" | "desc" = "asc") {
84 | return this.derive(
85 | orderBy(new ModelFieldGetter<"listId", Data, Todo>("listId"), direction)
86 | );
87 | }
88 |
89 | orderByText(direction: "asc" | "desc" = "asc") {
90 | return this.derive(
91 | orderBy(new ModelFieldGetter<"text", Data, Todo>("text"), direction)
92 | );
93 | }
94 |
95 | orderByCompleted(direction: "asc" | "desc" = "asc") {
96 | return this.derive(
97 | orderBy(
98 | new ModelFieldGetter<"completed", Data, Todo>("completed"),
99 | direction
100 | )
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/domain/generated/TodoSpec.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <4936b0ac00e412b24b18ade6c520a198>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import { Context } from "@aphro/runtime-ts";
7 | import { decodeModelData } from "@aphro/runtime-ts";
8 | import { encodeModelData } from "@aphro/runtime-ts";
9 | import { SID_of } from "@aphro/runtime-ts";
10 | import { NodeSpecWithCreate } from "@aphro/runtime-ts";
11 | import Todo from "../Todo.js";
12 | import { Data } from "./TodoBase.js";
13 |
14 | const fields = {
15 | id: {
16 | encoding: "none",
17 | },
18 | listId: {
19 | encoding: "none",
20 | },
21 | text: {
22 | encoding: "none",
23 | },
24 | completed: {
25 | encoding: "none",
26 | },
27 | } as const;
28 | const TodoSpec: NodeSpecWithCreate = {
29 | type: "node",
30 | createFrom(ctx: Context, data: Data, raw: boolean = true) {
31 | const existing = ctx.cache.get(data["id"], "todomvc", "todo");
32 | if (existing) {
33 | return existing;
34 | }
35 | if (raw) data = decodeModelData(data, fields);
36 | const result = new Todo(ctx, data);
37 | ctx.cache.set(data["id"], result, "todomvc", "todo");
38 | return result;
39 | },
40 |
41 | primaryKey: "id",
42 |
43 | storage: {
44 | engine: "sqlite",
45 | db: "todomvc",
46 | type: "sql",
47 | tablish: "todo",
48 | },
49 |
50 | fields,
51 |
52 | outboundEdges: {},
53 | };
54 |
55 | export default TodoSpec;
56 |
--------------------------------------------------------------------------------
/src/domain/generated/exports-node-sql.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <5cfc16eee05ef13cb14a1570b0e4ca91>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 |
7 | // @ts-ignore
8 | import * as path from "path";
9 | // @ts-ignore
10 | import * as fs from "fs";
11 |
12 | // @ts-ignore
13 | import { fileURLToPath } from "url";
14 |
15 | const __filename = fileURLToPath(import.meta.url);
16 | const __dirname = path.dirname(__filename);
17 |
18 | const [Todo, TodoList] = await Promise.all([
19 | fs.promises.readFile(path.join(__dirname, "Todo.sqlite.sql"), {
20 | encoding: "utf8",
21 | }),
22 | fs.promises.readFile(path.join(__dirname, "TodoList.sqlite.sql"), {
23 | encoding: "utf8",
24 | }),
25 | ]);
26 |
27 | export default {
28 | sqlite: {
29 | todomvc: {
30 | Todo,
31 | TodoList,
32 | },
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/src/domain/generated/exports-sql.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE:
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | import Todo from "./Todo.sqlite.sql?raw";
7 | import TodoList from "./TodoList.sqlite.sql?raw";
8 | export default {
9 | sqlite: {
10 | todomvc: {
11 | Todo,
12 | TodoList,
13 | },
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/domain/generated/exports.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <43cba4e780321beb1a115a65c06ed92e>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | export { default as Todo } from "../Todo.js";
7 | export { default as TodoList } from "../TodoList.js";
8 |
--------------------------------------------------------------------------------
/src/domain/generated/types.d.ts:
--------------------------------------------------------------------------------
1 | // SIGNED-SOURCE: <7946820adce4295e4ffad7d91d8edf9a>
2 | /**
3 | * AUTO-GENERATED FILE
4 | * Do not modify. Update your schema and re-generate for changes.
5 | */
6 | declare module "*.sql?raw";
7 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.sql";
2 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Aphrodite • TodoMVC
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { createRoot } from "react-dom/client";
3 |
4 | import { openDbAndCreateResolver } from "@aphro/wa-sqlite-connector";
5 | import { anonymous, bootstrap, sql } from "@aphro/runtime-ts";
6 | import TodoTable from "./domain/generated/Todo.sqlite.sql";
7 | import TodoListTable from "./domain/generated/TodoList.sqlite.sql";
8 | import { context, Context, sid } from "@aphro/runtime-ts";
9 | import App from "./App.js";
10 | import TodoList from "./domain/TodoList.js";
11 |
12 | openDbAndCreateResolver("todomvc")
13 | .then((resolver) => {
14 | const ctx = context(anonymous(), resolver);
15 | start(ctx);
16 | })
17 | .catch((e) => console.error(e));
18 |
19 | async function setup(ctx: Context): Promise {
20 | await bootstrap.createAutomigrateIfExists(ctx.dbResolver, {
21 | sqlite: {
22 | todomvc: {
23 | TodoList: TodoListTable,
24 | Todo: TodoTable,
25 | },
26 | },
27 | });
28 |
29 | let list = await TodoList.queryAll(ctx).genOnlyValue();
30 | if (list == null) {
31 | list = TodoList.create(ctx, {
32 | id: sid("aaaa"),
33 | filter: "all",
34 | editing: null,
35 | }).save().optimistic;
36 | }
37 |
38 | return list;
39 | }
40 |
41 | async function start(ctx: Context) {
42 | const list = await setup(ctx);
43 |
44 | const root = createRoot(document.getElementById("container")!);
45 | root.render();
46 | }
47 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | cursor: pointer;
22 | }
23 |
24 | body {
25 | font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
26 | line-height: 1.4em;
27 | background: #f5f5f5;
28 | color: #111111;
29 | min-width: 230px;
30 | max-width: 550px;
31 | margin: 0 auto;
32 | -webkit-font-smoothing: antialiased;
33 | -moz-osx-font-smoothing: grayscale;
34 | font-weight: 300;
35 | }
36 |
37 | :focus {
38 | outline: 0;
39 | }
40 |
41 | .hidden {
42 | display: none;
43 | }
44 |
45 | .todoapp {
46 | background: #fff;
47 | margin: 130px 0 40px 0;
48 | position: relative;
49 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
50 | }
51 |
52 | .todoapp input::-webkit-input-placeholder {
53 | font-style: italic;
54 | font-weight: 300;
55 | color: rgba(0, 0, 0, 0.4);
56 | }
57 |
58 | .todoapp input::-moz-placeholder {
59 | font-style: italic;
60 | font-weight: 300;
61 | color: rgba(0, 0, 0, 0.4);
62 | }
63 |
64 | .todoapp input::input-placeholder {
65 | font-style: italic;
66 | font-weight: 300;
67 | color: rgba(0, 0, 0, 0.4);
68 | }
69 |
70 | .todoapp h1 {
71 | position: absolute;
72 | top: -140px;
73 | width: 100%;
74 | font-size: 80px;
75 | font-weight: 200;
76 | text-align: center;
77 | color: #b83f45;
78 | -webkit-text-rendering: optimizeLegibility;
79 | -moz-text-rendering: optimizeLegibility;
80 | text-rendering: optimizeLegibility;
81 | }
82 |
83 | .new-todo,
84 | .edit {
85 | position: relative;
86 | margin: 0;
87 | width: 100%;
88 | font-size: 24px;
89 | font-family: inherit;
90 | font-weight: inherit;
91 | line-height: 1.4em;
92 | color: inherit;
93 | padding: 6px;
94 | border: 1px solid #999;
95 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
96 | box-sizing: border-box;
97 | -webkit-font-smoothing: antialiased;
98 | -moz-osx-font-smoothing: grayscale;
99 | }
100 |
101 | .new-todo {
102 | padding: 16px 16px 16px 60px;
103 | border: none;
104 | background: rgba(0, 0, 0, 0.003);
105 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
106 | }
107 |
108 | .main {
109 | position: relative;
110 | z-index: 2;
111 | border-top: 1px solid #e6e6e6;
112 | }
113 |
114 | .toggle-all {
115 | width: 1px;
116 | height: 1px;
117 | border: none; /* Mobile Safari */
118 | opacity: 0;
119 | position: absolute;
120 | right: 100%;
121 | bottom: 100%;
122 | }
123 |
124 | .toggle-all + label {
125 | width: 60px;
126 | height: 34px;
127 | font-size: 0;
128 | position: absolute;
129 | top: -52px;
130 | left: -13px;
131 | -webkit-transform: rotate(90deg);
132 | transform: rotate(90deg);
133 | }
134 |
135 | .toggle-all + label:before {
136 | content: "❯";
137 | font-size: 22px;
138 | color: #e6e6e6;
139 | padding: 10px 27px 10px 27px;
140 | }
141 |
142 | .toggle-all:checked + label:before {
143 | color: #737373;
144 | }
145 |
146 | .todo-list {
147 | margin: 0;
148 | padding: 0;
149 | list-style: none;
150 | }
151 |
152 | .todo-list li {
153 | position: relative;
154 | font-size: 24px;
155 | border-bottom: 1px solid #ededed;
156 | }
157 |
158 | .todo-list li:last-child {
159 | border-bottom: none;
160 | }
161 |
162 | .todo-list li.editing {
163 | border-bottom: none;
164 | padding: 0;
165 | }
166 |
167 | .todo-list li.editing .edit {
168 | display: block;
169 | width: calc(100% - 43px);
170 | padding: 12px 16px;
171 | margin: 0 0 0 43px;
172 | }
173 |
174 | .todo-list li.editing .view {
175 | display: none;
176 | }
177 |
178 | .todo-list li .toggle {
179 | text-align: center;
180 | width: 40px;
181 | /* auto, since non-WebKit browsers doesn't support input styling */
182 | height: auto;
183 | position: absolute;
184 | top: 0;
185 | bottom: 0;
186 | margin: auto 0;
187 | border: none; /* Mobile Safari */
188 | -webkit-appearance: none;
189 | appearance: none;
190 | }
191 |
192 | .todo-list li .toggle {
193 | opacity: 0;
194 | }
195 |
196 | .todo-list li .toggle + label {
197 | /*
198 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
199 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
200 | */
201 | background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E");
202 | background-repeat: no-repeat;
203 | background-position: center left;
204 | }
205 |
206 | .todo-list li .toggle:checked + label {
207 | background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E");
208 | }
209 |
210 | .todo-list li label {
211 | word-break: break-all;
212 | padding: 15px 15px 15px 60px;
213 | display: block;
214 | line-height: 1.2;
215 | transition: color 0.4s;
216 | font-weight: 400;
217 | color: #4d4d4d;
218 | }
219 |
220 | .todo-list li.completed label {
221 | color: #cdcdcd;
222 | text-decoration: line-through;
223 | }
224 |
225 | .todo-list li .destroy {
226 | display: none;
227 | position: absolute;
228 | top: 0;
229 | right: 10px;
230 | bottom: 0;
231 | width: 40px;
232 | height: 40px;
233 | margin: auto 0;
234 | font-size: 30px;
235 | color: #cc9a9a;
236 | margin-bottom: 11px;
237 | transition: color 0.2s ease-out;
238 | }
239 |
240 | .todo-list li .destroy:hover {
241 | color: #af5b5e;
242 | }
243 |
244 | .todo-list li .destroy:after {
245 | content: "×";
246 | }
247 |
248 | .todo-list li:hover .destroy {
249 | display: block;
250 | }
251 |
252 | .todo-list li .edit {
253 | display: none;
254 | }
255 |
256 | .todo-list li.editing:last-child {
257 | margin-bottom: -1px;
258 | }
259 |
260 | .footer {
261 | padding: 10px 15px;
262 | height: 20px;
263 | text-align: center;
264 | font-size: 15px;
265 | border-top: 1px solid #e6e6e6;
266 | }
267 |
268 | .footer:before {
269 | content: "";
270 | position: absolute;
271 | right: 0;
272 | bottom: 0;
273 | left: 0;
274 | height: 50px;
275 | overflow: hidden;
276 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
277 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
278 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
279 | }
280 |
281 | .todo-count {
282 | float: left;
283 | text-align: left;
284 | }
285 |
286 | .todo-count strong {
287 | font-weight: 300;
288 | }
289 |
290 | .filters {
291 | margin: 0;
292 | padding: 0;
293 | list-style: none;
294 | position: absolute;
295 | right: 0;
296 | left: 0;
297 | }
298 |
299 | .filters li {
300 | display: inline;
301 | }
302 |
303 | .filters li a {
304 | color: inherit;
305 | margin: 3px;
306 | padding: 3px 7px;
307 | text-decoration: none;
308 | border: 1px solid transparent;
309 | border-radius: 3px;
310 | cursor: pointer;
311 | }
312 |
313 | .filters li a:hover {
314 | border-color: rgba(175, 47, 47, 0.1);
315 | }
316 |
317 | .filters li a.selected {
318 | border-color: rgba(175, 47, 47, 0.2);
319 | }
320 |
321 | .clear-completed,
322 | html .clear-completed:active {
323 | float: right;
324 | position: relative;
325 | line-height: 20px;
326 | text-decoration: none;
327 | cursor: pointer;
328 | }
329 |
330 | .clear-completed:hover {
331 | text-decoration: underline;
332 | }
333 |
334 | .info {
335 | margin: 65px auto 0;
336 | color: #4d4d4d;
337 | font-size: 11px;
338 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
339 | text-align: center;
340 | }
341 |
342 | .info p {
343 | line-height: 1;
344 | }
345 |
346 | .info a {
347 | color: inherit;
348 | text-decoration: none;
349 | font-weight: 400;
350 | }
351 |
352 | .info a:hover {
353 | text-decoration: underline;
354 | }
355 |
356 | /*
357 | Hack to remove background from Mobile Safari.
358 | Can't use it globally since it destroys checkboxes in Firefox
359 | */
360 | @media screen and (-webkit-min-device-pixel-ratio: 0) {
361 | .toggle-all,
362 | .todo-list li .toggle {
363 | background: none;
364 | }
365 |
366 | .todo-list li .toggle {
367 | height: 40px;
368 | }
369 | }
370 |
371 | @media (max-width: 430px) {
372 | .footer {
373 | height: 50px;
374 | }
375 |
376 | .filters {
377 | bottom: 10px;
378 | }
379 | }
380 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/", // path to output directory
4 | "sourceMap": true, // allow sourcemap support
5 | "strictNullChecks": true, // enable strict null checks as a best practice
6 | "module": "esnext", // specify module code generation
7 | "target": "esnext", // specify ECMAScript target version
8 | "moduleResolution": "node",
9 | "rootDir": "./",
10 | "allowJs": true,
11 | "jsx": "react",
12 | "strict": true
13 | },
14 | "include": ["./src/"]
15 | }
16 |
--------------------------------------------------------------------------------
/wa-sqlite-async.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vlcn-io/orm-browser-starter/f45530fbba7d13298e999eda217bfe19cd1f523d/wa-sqlite-async.wasm
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | import HtmlWebpackPlugin from "html-webpack-plugin";
2 | import ResolveTypeScriptPlugin from "resolve-typescript-plugin";
3 | import path from "path";
4 | import { fileURLToPath } from "url";
5 |
6 | const __filename = fileURLToPath(import.meta.url);
7 | const __dirname = path.dirname(__filename);
8 |
9 | export default {
10 | experiments: {
11 | topLevelAwait: true,
12 | },
13 | devServer: {
14 | static: {
15 | directory: path.resolve(__dirname),
16 | publicPath: "/",
17 | },
18 | allowedHosts: "all",
19 | hot: true,
20 | },
21 | entry: "./src/index.tsx",
22 | mode: "development",
23 | resolve: {
24 | plugins: [new ResolveTypeScriptPlugin()],
25 | extensions: [".dev.js", ".js", ".json", ".wasm", "ts", "tsx"],
26 | fallback: {
27 | crypto: false,
28 | path: false,
29 | fs: false,
30 | },
31 | },
32 | plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })],
33 | module: {
34 | rules: [
35 | {
36 | test: /\.sql$/i,
37 | use: "raw-loader",
38 | },
39 | {
40 | test: /\.tsx?$/,
41 | use: "ts-loader",
42 | exclude: /node_modules/,
43 | },
44 | ],
45 | },
46 | };
47 |
--------------------------------------------------------------------------------