tangled
alpha
login
or
join now
adam.tngl.sh
/
photos
0
fork
atom
an attempt at a lightweight photo/album viewer
0
fork
atom
overview
issues
pulls
1
pipelines
pretty powerful relation functionality from drizzle
adam.tngl.sh
2 months ago
5457754f
534b45b1
+76
-33
7 changed files
expand all
collapse all
unified
split
server
deno.json
deno.lock
runtimes
dev.ts
src
albums-segmented.ts
albums.ts
db
schema
index.ts
photo.ts
+2
-2
server/deno.json
···
11
11
"drizzle-orm": "npm:drizzle-orm@^1.0.0-beta.6-c513c71",
12
12
"drizzle-zod": "npm:drizzle-zod@^0.8.3",
13
13
"eslint": "npm:eslint@^9.39.2",
14
14
-
"hono": "npm:hono@^4.11.0",
14
14
+
"hono": "npm:hono@^4.11.3",
15
15
"hono-pino": "npm:hono-pino@^0.10.3",
16
16
"libsql": "npm:libsql@^0.5.22",
17
17
"pino": "npm:pino@^10.1.0",
···
22
22
"zod": "npm:zod@^4.1.13"
23
23
},
24
24
"tasks": {
25
25
-
"generate": "deno run runtimes/generate.ts",
25
25
+
"generate": "mkdir build; deno run runtimes/generate.ts",
26
26
"dev": "deno run $(deno task allow-native-sql) --allow-read=.env --allow-net=0.0.0.0:8000 --allow-sys=hostname --allow-env=LIBSQL_JS_DEV,CI,TERM,NODE_V8_COVERAGE runtimes/dev.ts",
27
27
"serve": "deno run $(deno task allow-native-sql) --allow-read=.env --allow-net=0.0.0.0:8000 --allow-env=LIBSQL_JS_DEV runtimes/main.ts",
28
28
"lint": "eslint .",
+13
-13
server/deno.lock
···
4
4
"jsr:@std/dotenv@*": "0.225.5",
5
5
"jsr:@std/dotenv@~0.225.5": "0.225.5",
6
6
"npm:@antfu/eslint-config@^6.6.1": "6.6.1_eslint@9.39.2_@typescript-eslint+parser@8.49.0__eslint@9.39.2__typescript@5.9.3_typescript@5.9.3_@typescript-eslint+eslint-plugin@8.49.0__@typescript-eslint+parser@8.49.0___eslint@9.39.2___typescript@5.9.3__eslint@9.39.2__typescript@5.9.3_@stylistic+eslint-plugin@5.6.1__eslint@9.39.2_vue-eslint-parser@10.2.0__eslint@9.39.2",
7
7
-
"npm:@hono/zod-openapi@^1.1.5": "1.1.5_hono@4.11.0_zod@4.1.13",
7
7
+
"npm:@hono/zod-openapi@^1.1.5": "1.1.5_hono@4.11.3_zod@4.1.13",
8
8
"npm:@libsql/client@~0.15.15": "0.15.15",
9
9
-
"npm:@scalar/hono-api-reference@~0.9.28": "0.9.28_hono@4.11.0",
9
9
+
"npm:@scalar/hono-api-reference@~0.9.28": "0.9.28_hono@4.11.3",
10
10
"npm:@types/deno@^2.5.0": "2.5.0",
11
11
"npm:drizzle-orm@^1.0.0-beta.6-c513c71": "1.0.0-beta.6-c513c71_@libsql+client@0.15.15_@types+mssql@9.1.8_mssql@11.0.1_postgres@3.4.7",
12
12
"npm:drizzle-zod@~0.8.3": "0.8.3_drizzle-orm@1.0.0-beta.6-c513c71__@libsql+client@0.15.15__@types+mssql@9.1.8__mssql@11.0.1__postgres@3.4.7_zod@4.1.13_@libsql+client@0.15.15_postgres@3.4.7",
13
13
"npm:eslint@^9.39.2": "9.39.2",
14
14
-
"npm:hono-pino@~0.10.3": "0.10.3_hono@4.11.0_pino@10.1.0",
15
15
-
"npm:hono@^4.11.0": "4.11.0",
14
14
+
"npm:hono-pino@~0.10.3": "0.10.3_hono@4.11.3_pino@10.1.0",
15
15
+
"npm:hono@^4.11.3": "4.11.3",
16
16
"npm:libsql@~0.5.22": "0.5.22",
17
17
"npm:pino-pretty@^13.1.3": "13.1.3",
18
18
"npm:pino@^10.1.0": "10.1.0",
19
19
"npm:postgres@^3.4.7": "3.4.7",
20
20
-
"npm:stoker@^2.0.1": "2.0.1_@hono+zod-openapi@1.1.5__hono@4.11.0__zod@4.1.13_hono@4.11.0_zod@4.1.13",
20
20
+
"npm:stoker@^2.0.1": "2.0.1_@hono+zod-openapi@1.1.5__hono@4.11.3__zod@4.1.13_hono@4.11.3_zod@4.1.13",
21
21
"npm:typescript@^5.9.3": "5.9.3",
22
22
"npm:zod@^4.1.13": "4.1.13"
23
23
},
···
387
387
"levn"
388
388
]
389
389
},
390
390
-
"@hono/zod-openapi@1.1.5_hono@4.11.0_zod@4.1.13": {
390
390
+
"@hono/zod-openapi@1.1.5_hono@4.11.3_zod@4.1.13": {
391
391
"integrity": "sha512-EAnY6ad4yt/MUKHx716BEGGOXSl5d0/FOLozOYB/pmSEFq07qrzefKFtBEMAgd3hlpJXjH+4lwgTtlAo+BGBgQ==",
392
392
"dependencies": [
393
393
"@asteasolutions/zod-to-openapi",
···
397
397
"zod"
398
398
]
399
399
},
400
400
-
"@hono/zod-validator@0.7.5_hono@4.11.0_zod@4.1.13": {
400
400
+
"@hono/zod-validator@0.7.5_hono@4.11.3_zod@4.1.13": {
401
401
"integrity": "sha512-n4l4hutkfYU07PzRUHBOVzUEn38VSfrh+UVE5d0w4lyfWDOEhzxIupqo5iakRiJL44c3vTuFJBvcmUl8b9agIA==",
402
402
"dependencies": [
403
403
"hono",
···
521
521
"@scalar/types"
522
522
]
523
523
},
524
524
-
"@scalar/hono-api-reference@0.9.28_hono@4.11.0": {
524
524
+
"@scalar/hono-api-reference@0.9.28_hono@4.11.3": {
525
525
"integrity": "sha512-RVY55Rpcy9/irv0SMSxuSlQ6wDyuP+iTDmTz/d5tGv/qqo8vEOJMdDRNftMUqdtqiZUAE8fXJnuDCTJ80ZztAQ==",
526
526
"dependencies": [
527
527
"@scalar/core",
···
1523
1523
"help-me@5.0.0": {
1524
1524
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="
1525
1525
},
1526
1526
-
"hono-pino@0.10.3_hono@4.11.0_pino@10.1.0": {
1526
1526
+
"hono-pino@0.10.3_hono@4.11.3_pino@10.1.0": {
1527
1527
"integrity": "sha512-n0RNPIFOoq25Fg8b4D5gus4sVqI0z+8I17ibl96+p43d07UnZ0EMM/It0qSgfc7UtaC+XP5FkFmRHwBp6owsNA==",
1528
1528
"dependencies": [
1529
1529
"defu",
···
1531
1531
"pino"
1532
1532
]
1533
1533
},
1534
1534
-
"hono@4.11.0": {
1535
1535
-
"integrity": "sha512-Jg8uZzN2ul8/qlyid5FO8O624F3AK0wKtkgoeEON1qBum1rM1itYBxoMKu/1SPJC7F1+xlIZsJMmc4HHhJ1AWg=="
1534
1534
+
"hono@4.11.3": {
1535
1535
+
"integrity": "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="
1536
1536
},
1537
1537
"html-entities@2.6.0": {
1538
1538
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="
···
2554
2554
"sprintf-js@1.1.3": {
2555
2555
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
2556
2556
},
2557
2557
-
"stoker@2.0.1_@hono+zod-openapi@1.1.5__hono@4.11.0__zod@4.1.13_hono@4.11.0_zod@4.1.13": {
2557
2557
+
"stoker@2.0.1_@hono+zod-openapi@1.1.5__hono@4.11.3__zod@4.1.13_hono@4.11.3_zod@4.1.13": {
2558
2558
"integrity": "sha512-liSQNnJmn8fWSEan7sVaFe6iSHuN3X02fDGLS6snwW+FUuKi5HmKUHm3P+Kzr5xiDPqRpmSTtmGEBbSL9H2zkQ==",
2559
2559
"dependencies": [
2560
2560
"@hono/zod-openapi",
···
2804
2804
"npm:drizzle-zod@~0.8.3",
2805
2805
"npm:eslint@^9.39.2",
2806
2806
"npm:hono-pino@~0.10.3",
2807
2807
-
"npm:hono@^4.11.0",
2807
2807
+
"npm:hono@^4.11.3",
2808
2808
"npm:libsql@~0.5.22",
2809
2809
"npm:pino-pretty@^13.1.3",
2810
2810
"npm:pino@^10.1.0",
+12
-2
server/runtimes/dev.ts
···
1
1
+
import openapiConfig from '@/openapi.config.ts'
2
2
+
import { PhotoAPI } from '@/src/app.ts'
1
3
import type { AppBindings } from '@/types.ts'
2
4
import { OpenAPIHono } from '@hono/zod-openapi'
3
5
import { Scalar } from '@scalar/hono-api-reference'
4
6
import { pinoLogger } from 'hono-pino'
7
7
+
import { cors } from 'hono/cors'
5
8
import pino from 'pino'
6
9
import pretty from 'pino-pretty'
7
10
import defaultHook from 'stoker/openapi/default-hook'
8
8
-
import openapiConfig from '@/openapi.config.ts'
9
9
-
import { PhotoAPI } from '@/src/app.ts'
10
11
11
12
function devTools(app: OpenAPIHono<AppBindings>) {
12
13
app.use(pinoLogger({
13
14
pino: pino(pretty()),
14
15
}))
16
16
+
17
17
+
app.use(
18
18
+
'*', // This applies the middleware to all routes
19
19
+
cors({
20
20
+
origin: ['http://localhost:8300'], // Add your frontend development URLs
21
21
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Explicitly allow necessary methods
22
22
+
credentials: true, // Allow cookies/credentials if needed
23
23
+
})
24
24
+
);
15
25
16
26
app.doc('/doc', openapiConfig)
17
27
+8
-1
server/src/albums-segmented.ts
···
10
10
import createErrorSchema from "stoker/openapi/schemas/create-error-schema";
11
11
import type { Passthrough } from "./app.ts";
12
12
import { albumToPhoto, photo } from "./db/schema/index.ts";
13
13
+
import { photoMetadata } from "./db/schema/photo.ts";
13
14
14
15
export const routes: Array<Passthrough> = [
15
16
(app) =>
···
64
65
65
66
const createdPhotos = await db.insert(photo).values(imgs).returning();
66
67
68
68
+
await db.insert(photoMetadata).values(createdPhotos.map((r, index) => {
69
69
+
return {
70
70
+
photoId: r.id,
71
71
+
...segment.images[index].metadata
72
72
+
}
73
73
+
}))
74
74
+
67
75
await db.insert(segmentToPhoto).values(createdPhotos.map((p) => {
68
76
return {
69
77
segmentId: createdSegment.id,
···
86
94
const segmentedAlbumWithNewPhotosWBluntGeo = z.object({
87
95
album: createInsertSchema(albumTb).omit({
88
96
id: true,
89
89
-
cover: true,
90
97
}),
91
98
sections: z.array(z.object({
92
99
segments: z.array(z.object({
+22
-8
server/src/albums.ts
···
9
9
import createErrorSchema from "stoker/openapi/schemas/create-error-schema";
10
10
import IdParamsSchema from "stoker/openapi/schemas/id-params";
11
11
import type { App, Passthrough } from "./app.ts";
12
12
-
import { albumToPhoto, photo } from "./db/schema/index.ts";
12
12
+
import { albumSection, albumSegment, albumToPhoto, photo } from "./db/schema/index.ts";
13
13
+
import { photoMetadata } from "./db/schema/photo.ts";
13
14
14
15
export const routes: Array<Passthrough> = [
15
16
(app) =>
···
33
34
},
34
35
responses: {
35
36
[HttpStatusCodes.OK]: jsonContent(
36
36
-
createSelectSchema(albumTb),
37
37
+
createSelectSchema(albumTb).extend({
38
38
+
sections: z.array(createSelectSchema(albumSection).pick({
39
39
+
id: true
40
40
+
}).extend({
41
41
+
segments: z.array(createSelectSchema(albumSegment).extend({
42
42
+
images: z.array(createSelectSchema(photo).extend({
43
43
+
metadata: createSelectSchema(photoMetadata)
44
44
+
}))
45
45
+
}))
46
46
+
}))
47
47
+
}),
37
48
"an album",
38
49
),
39
39
-
[HttpStatusCodes.UNPROCESSABLE_ENTITY]: jsonContent(
40
40
-
createErrorSchema(createSelectSchema(albumTb)),
41
41
-
"validation errors",
42
42
-
),
43
50
},
44
51
}),
45
52
async (c) => {
···
55
62
id: true,
56
63
},
57
64
with: {
58
58
-
segments: true,
65
65
+
segments: {
66
66
+
with: {
67
67
+
images: {
68
68
+
with: {
69
69
+
metadata: true
70
70
+
}
71
71
+
}
72
72
+
}
73
73
+
}
59
74
},
60
75
},
61
76
},
···
173
188
contents: z.array(
174
189
createInsertSchema(photo).omit({
175
190
id: true,
176
176
-
takenWith: true,
177
191
}),
178
192
),
179
193
});
+14
-2
server/src/db/schema/index.ts
···
1
1
import { defineRelations } from "drizzle-orm";
2
2
-
import { album, albumSection, albumSegment } from "./album.ts";
3
3
-
import { photo } from './photo.ts';
2
2
+
import { album, albumSection, albumSegment, segmentToPhoto } from "./album.ts";
3
3
+
import { photo, photoMetadata } from './photo.ts';
4
4
5
5
export const relations = defineRelations({
6
6
+
photoMetadata,
6
7
album,
7
8
albumSection,
8
9
albumSegment,
10
10
+
segmentToPhoto,
9
11
photo
10
12
}, (r) => ({
13
13
+
photo: {
14
14
+
metadata: r.one.photoMetadata({
15
15
+
from: r.photo.id,
16
16
+
to: r.photoMetadata.photoId
17
17
+
})
18
18
+
},
11
19
albumSegment: {
12
20
parent: r.one.albumSection({
13
21
from: r.albumSegment.sectionId,
14
22
to: r.albumSection.id
23
23
+
}),
24
24
+
images: r.many.photo({
25
25
+
from: r.albumSegment.id.through(r.segmentToPhoto.segmentId),
26
26
+
to: r.photo.id.through(r.segmentToPhoto.photoId)
15
27
})
16
28
},
17
29
albumSection: {
+5
-5
server/src/db/schema/photo.ts
···
1
1
-
import { defineRelations } from "drizzle-orm";
2
1
import { int, integer, real, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core';
3
2
4
3
export const photo = sqliteTable('photo', {
···
6
5
fileName: text().notNull(),
7
6
dateCreated: int().notNull(),
8
7
dateModified: int().notNull(),
8
8
+
})
9
9
+
10
10
+
export const photoMetadata = sqliteTable('photoMetadata', {
11
11
+
photoId: integer({ mode: 'number' }).notNull().references(() => photo.id).primaryKey(),
9
12
lat: real(),
10
13
lon: real(),
11
14
width: int().notNull(),
···
18
21
const timeline = sqliteView("timeline_view").as((qb) => qb
19
22
.select()
20
23
.from(photo)
21
21
-
);
22
22
-
23
23
-
24
24
-
export const relations = defineRelations({ photo })
24
24
+
);