@things-factory/figure-service 10.1.112 → 10.1.113
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/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +2 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/routers/figure-thumbnail-router.js +12 -5
- package/dist-server/routers/figure-thumbnail-router.js.map +1 -1
- package/dist-server/service/figure/figure-thumbnail-reader.d.ts +7 -0
- package/dist-server/service/figure/figure-thumbnail-reader.js +21 -0
- package/dist-server/service/figure/figure-thumbnail-reader.js.map +1 -0
- package/dist-server/service/figure/figure-thumbnail-store.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/server/index.ts +2 -0
- package/server/routers/figure-thumbnail-router.ts +11 -4
- package/server/service/figure/figure-thumbnail-db.test.ts +20 -2
- package/server/service/figure/figure-thumbnail-reader.ts +20 -0
- package/server/service/figure/figure-thumbnail-store.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-service",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.113",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"@things-factory:registry": "https://registry.npmjs.org"
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"@hatiolab/figure-model": "^0.2.10",
|
|
29
29
|
"@operato/typeorm-history": "^10.0.0",
|
|
30
30
|
"@things-factory/ai-client-base": "^10.1.81",
|
|
31
|
-
"@things-factory/attachment-base": "^10.1.
|
|
32
|
-
"@things-factory/auth-base": "^10.1.
|
|
31
|
+
"@things-factory/attachment-base": "^10.1.113",
|
|
32
|
+
"@things-factory/auth-base": "^10.1.113",
|
|
33
33
|
"@things-factory/env": "^10.1.20",
|
|
34
34
|
"@things-factory/shell": "^10.1.106",
|
|
35
35
|
"graphql-scalars": "^1.22.4",
|
|
36
36
|
"graphql-upload": "^15.0.2",
|
|
37
37
|
"koa-router": "^7.4.0"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "9da1e467ddce23e600e64f08454289cb3b806212"
|
|
40
40
|
}
|
package/server/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ process.on('bootstrap-module-start' as any, () => {
|
|
|
26
26
|
*/
|
|
27
27
|
import { getDataSource, inBootTurn } from '@things-factory/shell'
|
|
28
28
|
import { moveBase64Thumbnails } from './service/figure/figure-thumbnail-upgrade.js'
|
|
29
|
+
/* Who may read a figure picture by its attachment path (ADR-0106): registered with attachment-base at load. */
|
|
30
|
+
import './service/figure/figure-thumbnail-reader.js'
|
|
29
31
|
process.on('bootstrap-module-start' as any, () => {
|
|
30
32
|
inBootTurn(() => moveBase64Thumbnails(getDataSource()))
|
|
31
33
|
.then(moved => {
|
|
@@ -105,15 +105,22 @@ export async function sendFigureThumbnail(
|
|
|
105
105
|
attachment 의 공개 경로가 쓰는 `public` 을 따라 쓰지 않는다(판정 조건 1). 같은 내용은 같은 해시이므로 ETag 가
|
|
106
106
|
곧 내용이다. 부르는 쪽이 `?v=` 로 판을 가리므로 오래 두어도 낡은 그림이 남지 않는다.
|
|
107
107
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
const ours = () => {
|
|
109
|
+
context.type = picture.mimetype
|
|
110
|
+
context.set('Cache-Control', 'private, max-age=31536000')
|
|
111
|
+
context.set('ETag', `"${found.thumbnailHash}"`)
|
|
112
|
+
if (found.thumbnailUpdatedAt) context.set('Last-Modified', new Date(found.thumbnailUpdatedAt).toUTCString())
|
|
113
|
+
}
|
|
114
|
+
ours()
|
|
115
|
+
/* koa answers `fresh` only for a 2xx: before a body the status is still 404, and the check never matched. */
|
|
116
|
+
context.status = 200
|
|
112
117
|
if (context.fresh) {
|
|
113
118
|
context.status = 304
|
|
114
119
|
return
|
|
115
120
|
}
|
|
116
121
|
await files.send(context, picture.path!, next)
|
|
122
|
+
/* The storage sets its own (S3 its ETag, attachment's Cache-Control): this door's are said last. */
|
|
123
|
+
ours()
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
@@ -37,7 +37,10 @@ function mapFiles() {
|
|
|
37
37
|
kept.set(path, bytes)
|
|
38
38
|
return { id, path, size: bytes.length }
|
|
39
39
|
},
|
|
40
|
+
/* As S3 does: its own tag and cache rule on the way out. */
|
|
40
41
|
async send(koa: any, path: string) {
|
|
42
|
+
koa.set('ETag', '"storage-md5"')
|
|
43
|
+
koa.set('Cache-Control', 'public, max-age=31536000')
|
|
41
44
|
koa.body = kept.get(path)
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -89,15 +92,16 @@ const attachments = (domainId?: string) =>
|
|
|
89
92
|
function koa(ifNoneMatch?: string) {
|
|
90
93
|
const headers: Record<string, string> = {}
|
|
91
94
|
return {
|
|
92
|
-
status:
|
|
95
|
+
status: 404,
|
|
93
96
|
type: '',
|
|
94
97
|
body: undefined as any,
|
|
95
98
|
headers,
|
|
96
99
|
set(k: string, v: string) {
|
|
97
100
|
headers[k] = v
|
|
98
101
|
},
|
|
102
|
+
/* As koa: fresh only for a 2xx or 304, and only when the request's tag is the response's. */
|
|
99
103
|
get fresh() {
|
|
100
|
-
return !!ifNoneMatch && ifNoneMatch === headers['ETag']
|
|
104
|
+
return ((this.status >= 200 && this.status < 300) || this.status === 304) && !!ifNoneMatch && ifNoneMatch === headers['ETag']
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
}
|
|
@@ -216,3 +220,17 @@ describe('the base64 an install holds, moved at start without losing one', () =>
|
|
|
216
220
|
expect((await attachments(domain.id)).length).toBe(3)
|
|
217
221
|
})
|
|
218
222
|
})
|
|
223
|
+
|
|
224
|
+
/* ADR-0106 decision 2: by its attachment path, a figure picture opens to whoever may query figures, as by its figure. */
|
|
225
|
+
describe('read by its attachment path', () => {
|
|
226
|
+
it('figure registers the reader for its pictures, and it asks the figure query permission', async () => {
|
|
227
|
+
const { figureThumbnailReader } = loadCompiled('service/figure/figure-thumbnail-reader')
|
|
228
|
+
expect(attachmentBase.attachmentReaderOf('figure-thumbnail')).toBeDefined()
|
|
229
|
+
const asked: any[] = []
|
|
230
|
+
const check = async (...args: any[]) => (asked.push(args), false)
|
|
231
|
+
const context = { state: { domain: { id: 'd' }, user: { id: 'u' }, unsafeIP: false, prohibitedPrivileges: [] } }
|
|
232
|
+
expect(await figureThumbnailReader({}, context, check)).toBe(false)
|
|
233
|
+
expect(asked[0][0]).toEqual({ category: 'figure', privilege: 'query', owner: true })
|
|
234
|
+
expect(await figureThumbnailReader({}, context, async () => true)).toBe(true)
|
|
235
|
+
})
|
|
236
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { registerAttachmentReader } from '@things-factory/attachment-base'
|
|
2
|
+
import { checkPermission } from '@things-factory/auth-base'
|
|
3
|
+
|
|
4
|
+
import { THUMBNAIL_REF_TYPE } from './figure-thumbnail-store.js'
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Apart from the store, and loaded only by the server's entry: the store is read by pure modules (the save path,
|
|
8
|
+
* figure-ui's tests), and auth-base brings shell's routers with it.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Who may read a figure picture by its attachment path (ADR-0106 decision 2): whoever may query figures -- the same door
|
|
12
|
+
* `/figure-thumbnail/:id` keeps, so the picture is no more open by its path than by its figure. attachment-base has
|
|
13
|
+
* already checked the attachment is of the request's domain.
|
|
14
|
+
*/
|
|
15
|
+
export async function figureThumbnailReader(_attachment: unknown, context: any, check: typeof checkPermission = checkPermission): Promise<boolean> {
|
|
16
|
+
const { domain, user, unsafeIP, prohibitedPrivileges } = context?.state ?? {}
|
|
17
|
+
return !!(await check({ category: 'figure', privilege: 'query', owner: true }, user, domain, unsafeIP, prohibitedPrivileges))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
registerAttachmentReader(THUMBNAIL_REF_TYPE, (attachment, context) => figureThumbnailReader(attachment, context))
|