@things-factory/gallery 0.0.0-stage → 10.1.223
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/client/bootstrap.ts +16 -0
- package/client/components/gallery-forms.ts +188 -0
- package/client/components/gallery-uploader.ts +557 -0
- package/client/components/gallery-viewer.ts +397 -0
- package/client/index.ts +1 -0
- package/client/pages/gallery-page.ts +567 -0
- package/client/route.ts +8 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/gallery-api.ts +157 -0
- package/client/utils/gallery-badge.ts +44 -0
- package/client/utils/gallery-words.ts +22 -0
- package/client/utils/photo-prep.ts +93 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +16 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/components/gallery-forms.d.ts +31 -0
- package/dist-client/components/gallery-forms.js +223 -0
- package/dist-client/components/gallery-forms.js.map +1 -0
- package/dist-client/components/gallery-uploader.d.ts +51 -0
- package/dist-client/components/gallery-uploader.js +588 -0
- package/dist-client/components/gallery-uploader.js.map +1 -0
- package/dist-client/components/gallery-viewer.d.ts +30 -0
- package/dist-client/components/gallery-viewer.js +416 -0
- package/dist-client/components/gallery-viewer.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/pages/gallery-page.d.ts +55 -0
- package/dist-client/pages/gallery-page.js +586 -0
- package/dist-client/pages/gallery-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +9 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-client/utils/gallery-api.d.ts +95 -0
- package/dist-client/utils/gallery-api.js +46 -0
- package/dist-client/utils/gallery-api.js.map +1 -0
- package/dist-client/utils/gallery-badge.d.ts +4 -0
- package/dist-client/utils/gallery-badge.js +45 -0
- package/dist-client/utils/gallery-badge.js.map +1 -0
- package/dist-client/utils/gallery-words.d.ts +7 -0
- package/dist-client/utils/gallery-words.js +20 -0
- package/dist-client/utils/gallery-words.js.map +1 -0
- package/dist-client/utils/photo-prep.d.ts +10 -0
- package/dist-client/utils/photo-prep.js +102 -0
- package/dist-client/utils/photo-prep.js.map +1 -0
- package/dist-server/attachment-reader.d.ts +1 -0
- package/dist-server/attachment-reader.js +13 -0
- package/dist-server/attachment-reader.js.map +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +11 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +15 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/service/gallery/gallery-resolver.d.ts +29 -0
- package/dist-server/service/gallery/gallery-resolver.js +602 -0
- package/dist-server/service/gallery/gallery-resolver.js.map +1 -0
- package/dist-server/service/gallery/gallery-rules.d.ts +88 -0
- package/dist-server/service/gallery/gallery-rules.js +181 -0
- package/dist-server/service/gallery/gallery-rules.js.map +1 -0
- package/dist-server/service/gallery/gallery-service.d.ts +37 -0
- package/dist-server/service/gallery/gallery-service.js +234 -0
- package/dist-server/service/gallery/gallery-service.js.map +1 -0
- package/dist-server/service/gallery/gallery-types.d.ts +62 -0
- package/dist-server/service/gallery/gallery-types.js +227 -0
- package/dist-server/service/gallery/gallery-types.js.map +1 -0
- package/dist-server/service/gallery/gallery.d.ts +83 -0
- package/dist-server/service/gallery/gallery.js +330 -0
- package/dist-server/service/gallery/gallery.js.map +1 -0
- package/dist-server/service/gallery/index.d.ts +4 -0
- package/dist-server/service/gallery/index.js +8 -0
- package/dist-server/service/gallery/index.js.map +1 -0
- package/dist-server/service/index.d.ts +7 -0
- package/dist-server/service/index.js +23 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -4
- package/server/attachment-reader.ts +14 -0
- package/server/index.ts +9 -0
- package/server/role-tasks.ts +14 -0
- package/server/service/gallery/gallery-resolver.ts +541 -0
- package/server/service/gallery/gallery-rules.ts +187 -0
- package/server/service/gallery/gallery-service.ts +254 -0
- package/server/service/gallery/gallery-types.ts +151 -0
- package/server/service/gallery/gallery.ts +266 -0
- package/server/service/gallery/index.ts +5 -0
- package/server/service/index.ts +19 -0
- package/server/tsconfig.json +11 -0
- package/test/gallery-persistence.test.ts +372 -0
- package/test/gallery-rules.test.ts +140 -0
- package/test/open-doors.test.ts +62 -0
- package/test/photo-prep.test.ts +61 -0
- package/test/translations.test.ts +68 -0
- package/things-factory.config.js +8 -0
- package/translations/en.json +91 -0
- package/translations/es.json +91 -0
- package/translations/ja.json +91 -0
- package/translations/ko.json +91 -0
- package/translations/ms.json +91 -0
- package/translations/zh.json +91 -0
- package/README.md +0 -3
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The tables of the gallery (ADR-0165 decisions 2, 4-6): a post of photos, a like, an event its gallery is open for,
|
|
3
|
+
* and when a person last looked. The photos themselves are attachments hung on the post (`refType: 'Gallery Post'`);
|
|
4
|
+
* an event is calendar's.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
Column,
|
|
8
|
+
CreateDateColumn,
|
|
9
|
+
DeleteDateColumn,
|
|
10
|
+
Entity,
|
|
11
|
+
Index,
|
|
12
|
+
ManyToOne,
|
|
13
|
+
PrimaryGeneratedColumn,
|
|
14
|
+
RelationId,
|
|
15
|
+
UpdateDateColumn
|
|
16
|
+
} from 'typeorm'
|
|
17
|
+
import { Field, ID, Int, ObjectType, registerEnumType } from 'type-graphql'
|
|
18
|
+
|
|
19
|
+
import { User } from '@things-factory/auth-base'
|
|
20
|
+
import { Event } from '@things-factory/calendar'
|
|
21
|
+
import { Domain } from '@things-factory/shell'
|
|
22
|
+
|
|
23
|
+
/** Where a post lives. Both are seen by everyone of the domain - the space says only whose it is and where it shows. */
|
|
24
|
+
export enum GallerySpace {
|
|
25
|
+
/** The company's - events, workshops and work. */
|
|
26
|
+
COMPANY = 'COMPANY',
|
|
27
|
+
/** The person's own - their days, hobbies and pets. */
|
|
28
|
+
PERSONAL = 'PERSONAL'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
registerEnumType(GallerySpace, {
|
|
32
|
+
name: 'GallerySpace',
|
|
33
|
+
description: "Where a gallery post lives: the company's gallery or the person's own."
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
@Entity()
|
|
37
|
+
@Index('ix_gallery_post_0', (one: GalleryPost) => [one.domain, one.createdAt])
|
|
38
|
+
@Index('ix_gallery_post_1', (one: GalleryPost) => [one.domain, one.event])
|
|
39
|
+
@Index('ix_gallery_post_2', (one: GalleryPost) => [one.domain, one.creator])
|
|
40
|
+
@ObjectType({
|
|
41
|
+
description: 'A post of the gallery - one or more photos, a short caption and tags, seen by everyone of the domain.'
|
|
42
|
+
})
|
|
43
|
+
export class GalleryPost {
|
|
44
|
+
@PrimaryGeneratedColumn('uuid')
|
|
45
|
+
@Field(type => ID, { description: 'Post id.' })
|
|
46
|
+
readonly id: string
|
|
47
|
+
|
|
48
|
+
@ManyToOne(type => Domain)
|
|
49
|
+
@Field(type => Domain, { nullable: true, description: 'The domain the post is in.' })
|
|
50
|
+
domain?: Domain
|
|
51
|
+
|
|
52
|
+
@RelationId((one: GalleryPost) => one.domain)
|
|
53
|
+
domainId?: string
|
|
54
|
+
|
|
55
|
+
@Column({ default: GallerySpace.COMPANY })
|
|
56
|
+
@Field(type => GallerySpace, { description: "The company's gallery or the person's own." })
|
|
57
|
+
space: GallerySpace
|
|
58
|
+
|
|
59
|
+
@Column({ type: 'text', nullable: true })
|
|
60
|
+
@Field({ nullable: true, description: 'A short caption, plain text.' })
|
|
61
|
+
caption?: string
|
|
62
|
+
|
|
63
|
+
@Column()
|
|
64
|
+
@Field({ description: 'When the photos were taken - from the first photo, or when it was posted.' })
|
|
65
|
+
takenAt: Date
|
|
66
|
+
|
|
67
|
+
@Column('simple-json', { nullable: true })
|
|
68
|
+
@Field(type => [String], { nullable: true, description: 'The tags, without the # mark.' })
|
|
69
|
+
tags?: string[]
|
|
70
|
+
|
|
71
|
+
/* Read as its name, time and place (GalleryEventFacts) - the post's field never hands out the calendar's event. */
|
|
72
|
+
@ManyToOne(type => Event, { nullable: true })
|
|
73
|
+
event?: Event
|
|
74
|
+
|
|
75
|
+
@RelationId((one: GalleryPost) => one.event)
|
|
76
|
+
eventId?: string
|
|
77
|
+
|
|
78
|
+
@Column('int', { default: 0 })
|
|
79
|
+
@Field(type => Int, { description: 'How many people like it.' })
|
|
80
|
+
likeCount: number
|
|
81
|
+
|
|
82
|
+
@Column({ nullable: true })
|
|
83
|
+
@Field({ nullable: true, description: 'When a moderator hid it; hidden, only its owner and the moderators see it.' })
|
|
84
|
+
hiddenAt?: Date
|
|
85
|
+
|
|
86
|
+
@Column({ nullable: true })
|
|
87
|
+
hiddenReason?: string
|
|
88
|
+
|
|
89
|
+
@ManyToOne(type => User, { nullable: true })
|
|
90
|
+
hiddenBy?: User
|
|
91
|
+
|
|
92
|
+
@RelationId((one: GalleryPost) => one.hiddenBy)
|
|
93
|
+
hiddenById?: string
|
|
94
|
+
|
|
95
|
+
@CreateDateColumn()
|
|
96
|
+
@Field({ nullable: true, description: 'When it was posted.' })
|
|
97
|
+
createdAt?: Date
|
|
98
|
+
|
|
99
|
+
@UpdateDateColumn()
|
|
100
|
+
@Field({ nullable: true, description: 'When it last changed.' })
|
|
101
|
+
updatedAt?: Date
|
|
102
|
+
|
|
103
|
+
@DeleteDateColumn()
|
|
104
|
+
@Field({ nullable: true, description: 'When its owner deleted it.' })
|
|
105
|
+
deletedAt?: Date
|
|
106
|
+
|
|
107
|
+
@ManyToOne(type => User, { nullable: true })
|
|
108
|
+
@Field(type => User, { nullable: true, description: 'Who posted it - the owner.' })
|
|
109
|
+
creator?: User
|
|
110
|
+
|
|
111
|
+
@RelationId((one: GalleryPost) => one.creator)
|
|
112
|
+
creatorId?: string
|
|
113
|
+
|
|
114
|
+
@ManyToOne(type => User, { nullable: true })
|
|
115
|
+
@Field(type => User, { nullable: true, description: 'Who changed it last.' })
|
|
116
|
+
updater?: User
|
|
117
|
+
|
|
118
|
+
@RelationId((one: GalleryPost) => one.updater)
|
|
119
|
+
updaterId?: string
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Entity()
|
|
123
|
+
@Index('ix_gallery_like_0', (one: GalleryLike) => [one.post, one.user], { unique: true, where: '"deleted_at" IS NULL' })
|
|
124
|
+
@ObjectType({ description: 'One person liking one gallery post.' })
|
|
125
|
+
export class GalleryLike {
|
|
126
|
+
@PrimaryGeneratedColumn('uuid')
|
|
127
|
+
@Field(type => ID, { description: 'Like id.' })
|
|
128
|
+
readonly id: string
|
|
129
|
+
|
|
130
|
+
@ManyToOne(type => Domain)
|
|
131
|
+
domain?: Domain
|
|
132
|
+
|
|
133
|
+
@RelationId((one: GalleryLike) => one.domain)
|
|
134
|
+
domainId?: string
|
|
135
|
+
|
|
136
|
+
@ManyToOne(type => GalleryPost, { onDelete: 'CASCADE' })
|
|
137
|
+
post?: GalleryPost
|
|
138
|
+
|
|
139
|
+
@RelationId((one: GalleryLike) => one.post)
|
|
140
|
+
postId?: string
|
|
141
|
+
|
|
142
|
+
@ManyToOne(type => User)
|
|
143
|
+
@Field(type => User, { nullable: true, description: 'Who likes it.' })
|
|
144
|
+
user?: User
|
|
145
|
+
|
|
146
|
+
@RelationId((one: GalleryLike) => one.user)
|
|
147
|
+
userId?: string
|
|
148
|
+
|
|
149
|
+
@CreateDateColumn()
|
|
150
|
+
@Field({ nullable: true, description: 'When they liked it.' })
|
|
151
|
+
createdAt?: Date
|
|
152
|
+
|
|
153
|
+
@UpdateDateColumn()
|
|
154
|
+
updatedAt?: Date
|
|
155
|
+
|
|
156
|
+
@DeleteDateColumn()
|
|
157
|
+
deletedAt?: Date
|
|
158
|
+
|
|
159
|
+
@ManyToOne(type => User, { nullable: true })
|
|
160
|
+
creator?: User
|
|
161
|
+
|
|
162
|
+
@RelationId((one: GalleryLike) => one.creator)
|
|
163
|
+
creatorId?: string
|
|
164
|
+
|
|
165
|
+
@ManyToOne(type => User, { nullable: true })
|
|
166
|
+
updater?: User
|
|
167
|
+
|
|
168
|
+
@RelationId((one: GalleryLike) => one.updater)
|
|
169
|
+
updaterId?: string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A calendar event whose gallery is open - opened by a moderator making the event from the gallery, or by the first post
|
|
174
|
+
* linked to it. Everyone of the domain may post to an open event (ADR-0165 사용자 판정 1).
|
|
175
|
+
*/
|
|
176
|
+
@Entity()
|
|
177
|
+
@Index('ix_gallery_event_0', (one: GalleryEvent) => [one.domain, one.event], {
|
|
178
|
+
unique: true,
|
|
179
|
+
where: '"deleted_at" IS NULL'
|
|
180
|
+
})
|
|
181
|
+
@ObjectType({ description: 'A calendar event whose gallery is open to everyone of the domain.' })
|
|
182
|
+
export class GalleryEvent {
|
|
183
|
+
@PrimaryGeneratedColumn('uuid')
|
|
184
|
+
@Field(type => ID, { description: 'Gallery event id.' })
|
|
185
|
+
readonly id: string
|
|
186
|
+
|
|
187
|
+
@ManyToOne(type => Domain)
|
|
188
|
+
domain?: Domain
|
|
189
|
+
|
|
190
|
+
@RelationId((one: GalleryEvent) => one.domain)
|
|
191
|
+
domainId?: string
|
|
192
|
+
|
|
193
|
+
@ManyToOne(type => Event, { onDelete: 'CASCADE' })
|
|
194
|
+
event?: Event
|
|
195
|
+
|
|
196
|
+
@RelationId((one: GalleryEvent) => one.event)
|
|
197
|
+
eventId?: string
|
|
198
|
+
|
|
199
|
+
@CreateDateColumn()
|
|
200
|
+
createdAt?: Date
|
|
201
|
+
|
|
202
|
+
@UpdateDateColumn()
|
|
203
|
+
updatedAt?: Date
|
|
204
|
+
|
|
205
|
+
@DeleteDateColumn()
|
|
206
|
+
deletedAt?: Date
|
|
207
|
+
|
|
208
|
+
@ManyToOne(type => User, { nullable: true })
|
|
209
|
+
creator?: User
|
|
210
|
+
|
|
211
|
+
@RelationId((one: GalleryEvent) => one.creator)
|
|
212
|
+
creatorId?: string
|
|
213
|
+
|
|
214
|
+
@ManyToOne(type => User, { nullable: true })
|
|
215
|
+
updater?: User
|
|
216
|
+
|
|
217
|
+
@RelationId((one: GalleryEvent) => one.updater)
|
|
218
|
+
updaterId?: string
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** When a person last opened the gallery - the menu counts the posts that came after. */
|
|
222
|
+
@Entity()
|
|
223
|
+
@Index('ix_gallery_visit_0', (one: GalleryVisit) => [one.domain, one.user], {
|
|
224
|
+
unique: true,
|
|
225
|
+
where: '"deleted_at" IS NULL'
|
|
226
|
+
})
|
|
227
|
+
export class GalleryVisit {
|
|
228
|
+
@PrimaryGeneratedColumn('uuid')
|
|
229
|
+
readonly id: string
|
|
230
|
+
|
|
231
|
+
@ManyToOne(type => Domain)
|
|
232
|
+
domain?: Domain
|
|
233
|
+
|
|
234
|
+
@RelationId((one: GalleryVisit) => one.domain)
|
|
235
|
+
domainId?: string
|
|
236
|
+
|
|
237
|
+
@ManyToOne(type => User)
|
|
238
|
+
user?: User
|
|
239
|
+
|
|
240
|
+
@RelationId((one: GalleryVisit) => one.user)
|
|
241
|
+
userId?: string
|
|
242
|
+
|
|
243
|
+
@Column()
|
|
244
|
+
seenAt: Date
|
|
245
|
+
|
|
246
|
+
@CreateDateColumn()
|
|
247
|
+
createdAt?: Date
|
|
248
|
+
|
|
249
|
+
@UpdateDateColumn()
|
|
250
|
+
updatedAt?: Date
|
|
251
|
+
|
|
252
|
+
@DeleteDateColumn()
|
|
253
|
+
deletedAt?: Date
|
|
254
|
+
|
|
255
|
+
@ManyToOne(type => User, { nullable: true })
|
|
256
|
+
creator?: User
|
|
257
|
+
|
|
258
|
+
@RelationId((one: GalleryVisit) => one.creator)
|
|
259
|
+
creatorId?: string
|
|
260
|
+
|
|
261
|
+
@ManyToOne(type => User, { nullable: true })
|
|
262
|
+
updater?: User
|
|
263
|
+
|
|
264
|
+
@RelationId((one: GalleryVisit) => one.updater)
|
|
265
|
+
updaterId?: string
|
|
266
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* EXPORT ENTITY TYPES */
|
|
2
|
+
export * from './gallery/gallery'
|
|
3
|
+
export * from './gallery/gallery-rules'
|
|
4
|
+
export { GALLERY_REF_TYPE, visiblePost } from './gallery/gallery-service'
|
|
5
|
+
|
|
6
|
+
/* IMPORT ENTITIES AND RESOLVERS */
|
|
7
|
+
import { entities as GalleryEntities, resolvers as GalleryResolvers } from './gallery'
|
|
8
|
+
|
|
9
|
+
export const entities = [
|
|
10
|
+
/* ENTITIES */
|
|
11
|
+
...GalleryEntities
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
export const schema = {
|
|
15
|
+
resolverClasses: [
|
|
16
|
+
/* RESOLVER CLASSES */
|
|
17
|
+
...GalleryResolvers
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gallery's doors on sqlite with the real entities (ADR-0165): who sees a post, what a post may hang on, the first
|
|
3
|
+
* photo of an event told once, likes, tags, the owner's changes, a moderator hiding a post, and the menu's count.
|
|
4
|
+
*
|
|
5
|
+
* Reads the built server (`dist-server`) - type-graphql needs tsc's decorator metadata. Storing a photo is
|
|
6
|
+
* attachment-base's (its own tests); here a photo is a row.
|
|
7
|
+
*/
|
|
8
|
+
import 'reflect-metadata'
|
|
9
|
+
|
|
10
|
+
const { existsSync } = require('fs')
|
|
11
|
+
const { join } = require('path')
|
|
12
|
+
const { DataSource } = require('typeorm')
|
|
13
|
+
|
|
14
|
+
/* What notification was asked to send (ADR-0164) - the channels themselves are notification's tests. */
|
|
15
|
+
const told: any[] = []
|
|
16
|
+
jest.mock('@things-factory/notification', () => ({
|
|
17
|
+
sendNoticeWhenCommitted: (_context: any, notice: any) => told.push(notice),
|
|
18
|
+
noticeLinkOf: () => undefined
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
jest.mock('@things-factory/attachment-base', () => {
|
|
22
|
+
const actual = jest.requireActual('@things-factory/attachment-base')
|
|
23
|
+
const shell = jest.requireActual('@things-factory/shell')
|
|
24
|
+
const repository = (context: any) => shell.getRepository(actual.Attachment, context.state.tx)
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
createAttachment: async (_: any, { attachment }: any, context: any) => {
|
|
28
|
+
const file = await attachment.file
|
|
29
|
+
return repository(context).save({
|
|
30
|
+
domain: context.state.domain,
|
|
31
|
+
name: file.filename,
|
|
32
|
+
mimetype: file.mimetype,
|
|
33
|
+
encoding: '7bit',
|
|
34
|
+
refType: attachment.refType,
|
|
35
|
+
refBy: attachment.refBy,
|
|
36
|
+
path: `${attachment.refBy}/${file.filename}`,
|
|
37
|
+
size: '1',
|
|
38
|
+
category: 'image'
|
|
39
|
+
})
|
|
40
|
+
},
|
|
41
|
+
deleteAttachment: async (_: any, { id }: any, context: any) => !!(await repository(context).delete(id)),
|
|
42
|
+
deleteAttachmentsByRef: async (_: any, { refBys, refType }: any, context: any) => {
|
|
43
|
+
for (const refBy of refBys) await repository(context).delete({ refBy, refType })
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const shell = require('@things-factory/shell')
|
|
50
|
+
const authBase = require('@things-factory/auth-base')
|
|
51
|
+
const attachmentBase = require('@things-factory/attachment-base')
|
|
52
|
+
const calendar = require('@things-factory/calendar')
|
|
53
|
+
|
|
54
|
+
function loadCompiled(modulePath: string): any {
|
|
55
|
+
const resolved = join(__dirname, '..', 'dist-server', `${modulePath}.js`)
|
|
56
|
+
if (!existsSync(resolved))
|
|
57
|
+
throw new Error(`gallery build output is missing: ${resolved}\nBuild it first: npm run build:server`)
|
|
58
|
+
return require(resolved)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { GalleryResolver } = loadCompiled('service/gallery/gallery-resolver')
|
|
62
|
+
const { GalleryPost, GalleryLike, GalleryEvent, GalleryVisit } = loadCompiled('service/gallery/gallery')
|
|
63
|
+
|
|
64
|
+
let dataSource: any
|
|
65
|
+
let domain: any
|
|
66
|
+
let people: Record<string, any>
|
|
67
|
+
const doors = new GalleryResolver()
|
|
68
|
+
|
|
69
|
+
beforeAll(async () => {
|
|
70
|
+
dataSource = new DataSource({
|
|
71
|
+
type: 'sqlite',
|
|
72
|
+
database: ':memory:',
|
|
73
|
+
synchronize: true,
|
|
74
|
+
dropSchema: true,
|
|
75
|
+
namingStrategy: new shell.NamingStrategy(),
|
|
76
|
+
entities: [
|
|
77
|
+
...shell.entities,
|
|
78
|
+
...authBase.entities,
|
|
79
|
+
...attachmentBase.entities,
|
|
80
|
+
...calendar.entities,
|
|
81
|
+
GalleryPost,
|
|
82
|
+
GalleryLike,
|
|
83
|
+
GalleryEvent,
|
|
84
|
+
GalleryVisit
|
|
85
|
+
],
|
|
86
|
+
logging: false
|
|
87
|
+
})
|
|
88
|
+
await dataSource.initialize()
|
|
89
|
+
shell.addDataSource('default', dataSource)
|
|
90
|
+
shell.addDataSource('tx', dataSource)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
afterAll(async () => {
|
|
94
|
+
if (!dataSource) return
|
|
95
|
+
shell.removeDataSource('default')
|
|
96
|
+
shell.removeDataSource('tx')
|
|
97
|
+
await dataSource.destroy()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
/* mod - moderates · kim, lee - employees · left - an account no longer active · outsider - another domain's. */
|
|
101
|
+
beforeEach(async () => {
|
|
102
|
+
told.length = 0
|
|
103
|
+
await dataSource.synchronize(true)
|
|
104
|
+
domain = await dataSource.getRepository(shell.Domain).save({ name: 'gallery', subdomain: 'gallery' })
|
|
105
|
+
const other = await dataSource.getRepository(shell.Domain).save({ name: 'elsewhere', subdomain: 'elsewhere' })
|
|
106
|
+
const user = (name: string, domains: any[], status = 'activated') =>
|
|
107
|
+
dataSource.getRepository(authBase.User).save({ name, email: `${name}@gallery.test`, status, domains })
|
|
108
|
+
people = {
|
|
109
|
+
mod: await user('mod', [domain]),
|
|
110
|
+
kim: await user('kim', [domain]),
|
|
111
|
+
lee: await user('lee', [domain]),
|
|
112
|
+
left: await user('left', [domain]),
|
|
113
|
+
outsider: await user('outsider', [other])
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
/** A request of `who` - inside the transaction a door opens; moderating and reading the calendar as given. */
|
|
118
|
+
const as = (who: string, { moderator = false, calendarReader = false } = {}) => ({
|
|
119
|
+
state: {
|
|
120
|
+
domain,
|
|
121
|
+
user: people[who],
|
|
122
|
+
tx: dataSource.manager,
|
|
123
|
+
__galleryModerator: Promise.resolve(moderator),
|
|
124
|
+
__galleryCalendarReader: Promise.resolve(calendarReader)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
const photo = (filename = 'a.jpg', mimetype = 'image/jpeg') =>
|
|
128
|
+
Promise.resolve({ filename, mimetype, encoding: '7bit', createReadStream: () => null })
|
|
129
|
+
const post = (who: string, input: any = {}, photos = [photo()]) =>
|
|
130
|
+
doors.createGalleryPost({ space: 'COMPANY', ...input }, photos, as(who))
|
|
131
|
+
const listed = async (who: string, filter: any = {}, opts: any = {}) =>
|
|
132
|
+
(await doors.galleryPosts(filter, 0, 60, as(who, opts))).items.map((one: any) => one.caption)
|
|
133
|
+
|
|
134
|
+
async function eventOf(name: string, attendees: string[] = [], organizer = 'mod') {
|
|
135
|
+
return dataSource.transaction((tx: any) =>
|
|
136
|
+
calendar.createEvent(tx, domain, people[organizer], {
|
|
137
|
+
name,
|
|
138
|
+
startAt: new Date('2026-10-24T09:00:00Z'),
|
|
139
|
+
endAt: new Date('2026-10-25T18:00:00Z'),
|
|
140
|
+
attendees: attendees.map(who => ({ userId: people[who].id }))
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function refusal(promise: Promise<unknown>): Promise<string | undefined> {
|
|
146
|
+
try {
|
|
147
|
+
await promise
|
|
148
|
+
} catch (error: any) {
|
|
149
|
+
return error.extensions?.code
|
|
150
|
+
}
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe('who sees a post', () => {
|
|
155
|
+
it('★ everyone of the domain sees every post, the newest first', async () => {
|
|
156
|
+
await post('kim', { caption: '첫 사진' })
|
|
157
|
+
await post('lee', { caption: '내 고양이', space: 'PERSONAL' })
|
|
158
|
+
expect(await listed('mod')).toEqual(['내 고양이', '첫 사진'])
|
|
159
|
+
expect(await listed('kim', { space: 'COMPANY' })).toEqual(['첫 사진'])
|
|
160
|
+
expect(await listed('kim', { authorId: people.lee.id })).toEqual(['내 고양이'])
|
|
161
|
+
const [first] = (await doors.galleryPosts({}, 0, 1, as('mod'))).items
|
|
162
|
+
expect((await doors.creator(first))?.name).toBe('lee')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('★ the own gallery of someone who left goes from view; their company posts stay', async () => {
|
|
166
|
+
await post('left', { caption: '회사 사진' })
|
|
167
|
+
await post('left', { caption: '내 사진', space: 'PERSONAL' })
|
|
168
|
+
await dataSource.getRepository(authBase.User).update(people.left.id, { status: 'inactive' })
|
|
169
|
+
expect(await listed('kim')).toEqual(['회사 사진'])
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('★ a hidden post is seen by its owner and the moderators only, and its reason by them only', async () => {
|
|
173
|
+
const one = await post('kim', { caption: '가릴 사진' })
|
|
174
|
+
await doors.hideGalleryPost(one.id, '얼굴이 나온 분이 원치 않습니다', as('mod', { moderator: true }))
|
|
175
|
+
expect(await listed('lee')).toEqual([])
|
|
176
|
+
expect(await doors.galleryPost(one.id, as('lee'))).toBeUndefined()
|
|
177
|
+
expect(await listed('kim')).toEqual(['가릴 사진'])
|
|
178
|
+
expect(await listed('mod', {}, { moderator: true })).toEqual(['가릴 사진'])
|
|
179
|
+
const seen = await doors.galleryPost(one.id, as('kim'))
|
|
180
|
+
expect(await doors.hiddenReason(seen, as('kim'))).toBe('얼굴이 나온 분이 원치 않습니다')
|
|
181
|
+
await doors.unhideGalleryPost(one.id, as('mod', { moderator: true }))
|
|
182
|
+
expect(await listed('lee')).toEqual(['가릴 사진'])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('★ the owner is told why their post was hidden; a moderator hiding their own is not', async () => {
|
|
186
|
+
const one = await post('kim')
|
|
187
|
+
await doors.hideGalleryPost(one.id, '중복', as('mod', { moderator: true }))
|
|
188
|
+
expect(told.map(one => [one.label, one.receivers.map((r: any) => r.id)])).toEqual([
|
|
189
|
+
[`gallery hidden ${one.id}`, [people.kim.id]]
|
|
190
|
+
])
|
|
191
|
+
expect(told[0].words('en').body).toBe('Reason: 중복')
|
|
192
|
+
expect(await refusal(doors.hideGalleryPost(one.id, ' ', as('mod', { moderator: true })))).toBe(
|
|
193
|
+
'gallery.reason-required'
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('a post of another domain is no post', async () => {
|
|
198
|
+
const one = await post('kim')
|
|
199
|
+
const outsider = { state: { ...as('outsider').state, domain: { id: 'elsewhere' } } }
|
|
200
|
+
expect(await doors.galleryPost(one.id, outsider)).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('what a post holds', () => {
|
|
205
|
+
it('★ photos only, 1 to 20, and no event in a person’s own gallery', async () => {
|
|
206
|
+
expect(await refusal(post('kim', {}, []))).toBe('gallery.no-photo')
|
|
207
|
+
expect(await refusal(post('kim', {}, [photo('a.pdf', 'application/pdf')]))).toBe('gallery.not-a-photo')
|
|
208
|
+
expect(
|
|
209
|
+
await refusal(
|
|
210
|
+
post(
|
|
211
|
+
'kim',
|
|
212
|
+
{},
|
|
213
|
+
Array.from({ length: 21 }, () => photo())
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
).toBe('gallery.too-many-photos')
|
|
217
|
+
const event = await eventOf('가을 워크숍', ['kim'])
|
|
218
|
+
expect(await refusal(post('kim', { space: 'PERSONAL', eventId: event.id }))).toBe('gallery.event-in-personal')
|
|
219
|
+
expect(await dataSource.getRepository(GalleryPost).count()).toBe(0)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('keeps the tags as they are kept and the photos in their order', async () => {
|
|
223
|
+
const one = await post('kim', { caption: ' 가을 ', tags: ['#워크숍', '워크숍', ' 팀 '] }, [
|
|
224
|
+
photo('1.jpg'),
|
|
225
|
+
photo('2.jpg')
|
|
226
|
+
])
|
|
227
|
+
expect(one).toMatchObject({ caption: '가을', tags: ['워크숍', '팀'], likeCount: 0 })
|
|
228
|
+
expect((await doors.photos(one, as('kim'))).map((p: any) => p.name)).toEqual(['1.jpg', '2.jpg'])
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('an event and its gallery', () => {
|
|
233
|
+
it('★ posts only to an event one sees - and that post opens it to everyone', async () => {
|
|
234
|
+
const event = await eventOf('가을 워크숍', ['kim'])
|
|
235
|
+
expect(await refusal(post('lee', { eventId: event.id }))).toBe('gallery.unknown-event')
|
|
236
|
+
expect(
|
|
237
|
+
(await doors.galleryEventChoices(new Date('2026-10-24T12:00:00Z'), as('lee'))).map((one: any) => one.id)
|
|
238
|
+
).toEqual([])
|
|
239
|
+
|
|
240
|
+
await post('kim', { eventId: event.id, caption: '김의 사진' })
|
|
241
|
+
expect(await dataSource.getRepository(GalleryEvent).count()).toBe(1)
|
|
242
|
+
expect(
|
|
243
|
+
(await doors.galleryEventChoices(new Date('2026-10-24T12:00:00Z'), as('lee'))).map((one: any) => [
|
|
244
|
+
one.name,
|
|
245
|
+
one.near
|
|
246
|
+
])
|
|
247
|
+
).toEqual([['가을 워크숍', true]])
|
|
248
|
+
await post('lee', { eventId: event.id, caption: '이의 사진' })
|
|
249
|
+
expect(await listed('mod', { eventId: event.id })).toEqual(['이의 사진', '김의 사진'])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('★ a calendar reader may post to any event', async () => {
|
|
253
|
+
const event = await eventOf('이사회', [])
|
|
254
|
+
expect(
|
|
255
|
+
await refusal(doors.createGalleryPost({ eventId: event.id }, [photo()], as('lee', { calendarReader: true })))
|
|
256
|
+
).toBeUndefined()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('★ the first photo is told once - to the people named on it, never the poster', async () => {
|
|
260
|
+
const event = await eventOf('가을 워크숍', ['kim', 'lee'])
|
|
261
|
+
await post('kim', { eventId: event.id, caption: '단풍' })
|
|
262
|
+
await post('lee', { eventId: event.id })
|
|
263
|
+
expect(told).toHaveLength(1)
|
|
264
|
+
expect(told[0].receivers.map((r: any) => r.id)).toEqual([people.lee.id])
|
|
265
|
+
expect(told[0].address).toBe(`gallery-page?event=${event.id}`)
|
|
266
|
+
expect(told[0].words('en')).toMatchObject({
|
|
267
|
+
subject: 'Photos of 가을 워크숍 are up',
|
|
268
|
+
body: 'kim posted the first photos of 가을 워크숍.\n\n단풍'
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('★ an event made from the gallery names nobody - its first photo is told to everyone of the domain who is active', async () => {
|
|
273
|
+
const made = await doors.createGalleryEvent(
|
|
274
|
+
{ name: '송년회', startAt: new Date('2026-12-20T10:00:00Z'), endAt: new Date('2026-12-20T13:00:00Z') },
|
|
275
|
+
as('mod', { moderator: true })
|
|
276
|
+
)
|
|
277
|
+
await dataSource.getRepository(authBase.User).update(people.left.id, { status: 'inactive' })
|
|
278
|
+
await post('kim', { eventId: made.id })
|
|
279
|
+
expect(told[0].receivers.map((r: any) => r.id).sort()).toEqual([people.mod.id, people.lee.id].sort())
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('lists the open events with their posts counted and the most liked post’s first photo shown', async () => {
|
|
283
|
+
const event = await eventOf('가을 워크숍', ['kim', 'lee'])
|
|
284
|
+
await post('kim', { eventId: event.id }, [photo('k1.jpg'), photo('k2.jpg')])
|
|
285
|
+
const liked = await post('lee', { eventId: event.id }, [photo('l1.jpg')])
|
|
286
|
+
await doors.likeGalleryPost(liked.id, true, as('kim'))
|
|
287
|
+
const [card] = await doors.galleryEvents(as('mod'))
|
|
288
|
+
expect(card).toMatchObject({ name: '가을 워크숍', postCount: 2 })
|
|
289
|
+
expect(card.cover.name).toBe('l1.jpg')
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe('likes', () => {
|
|
294
|
+
it('★ a like counts once, and is taken back', async () => {
|
|
295
|
+
const one = await post('kim')
|
|
296
|
+
expect(await doors.likeGalleryPost(one.id, true, as('lee'))).toMatchObject({ likeCount: 1, liked: true })
|
|
297
|
+
expect(await doors.likeGalleryPost(one.id, true, as('lee'))).toMatchObject({ likeCount: 1 })
|
|
298
|
+
await doors.likeGalleryPost(one.id, true, as('mod'))
|
|
299
|
+
expect((await doors.galleryLikers(one.id, as('kim'))).map((p: any) => p.name).sort()).toEqual(['lee', 'mod'])
|
|
300
|
+
expect(await doors.likeGalleryPost(one.id, false, as('lee'))).toMatchObject({ likeCount: 1, liked: false })
|
|
301
|
+
expect((await doors.galleryPost(one.id, as('mod'))).__liked).toBe(true)
|
|
302
|
+
expect((await doors.galleryPost(one.id, as('lee'))).__liked).toBe(false)
|
|
303
|
+
expect(told).toHaveLength(0)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('tags', () => {
|
|
308
|
+
it('★ finds a post by a whole tag, whatever its case', async () => {
|
|
309
|
+
await post('kim', { caption: 'a', tags: ['Workshop'] })
|
|
310
|
+
await post('kim', { caption: 'b', tags: ['Workshop2026'] })
|
|
311
|
+
expect(await listed('lee', { tag: '#workshop' })).toEqual(['a'])
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('★ a moderator renames a tag on every post, merging it into one already there', async () => {
|
|
315
|
+
await post('kim', { caption: 'a', tags: ['워크숍2026', '팀'] })
|
|
316
|
+
await post('lee', { caption: 'b', tags: ['2026워크숍'] })
|
|
317
|
+
expect(await doors.renameGalleryTag('워크숍2026', '#2026워크숍', as('mod', { moderator: true }))).toBe(1)
|
|
318
|
+
expect(await doors.galleryTags(undefined, as('kim'))).toEqual([
|
|
319
|
+
{ tag: '2026워크숍', count: 2 },
|
|
320
|
+
{ tag: '팀', count: 1 }
|
|
321
|
+
])
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe("one's own post", () => {
|
|
326
|
+
it('★ only its owner changes or deletes it, and at least one photo stays', async () => {
|
|
327
|
+
const one = await post('kim', { caption: '처음' }, [photo('1.jpg'), photo('2.jpg')])
|
|
328
|
+
expect(await refusal(doors.updateGalleryPost(one.id, { caption: '남이' }, [], [], as('lee')))).toBe(
|
|
329
|
+
'gallery.not-yours'
|
|
330
|
+
)
|
|
331
|
+
expect(await refusal(doors.deleteGalleryPost(one.id, as('mod', { moderator: true })))).toBe('gallery.not-yours')
|
|
332
|
+
const photos = await doors.photos(one, as('kim'))
|
|
333
|
+
expect(
|
|
334
|
+
await refusal(
|
|
335
|
+
doors.updateGalleryPost(
|
|
336
|
+
one.id,
|
|
337
|
+
{},
|
|
338
|
+
[],
|
|
339
|
+
photos.map((p: any) => p.id),
|
|
340
|
+
as('kim')
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
).toBe('gallery.no-photo')
|
|
344
|
+
const changed = await doors.updateGalleryPost(
|
|
345
|
+
one.id,
|
|
346
|
+
{ caption: '고침', tags: ['봄'] },
|
|
347
|
+
[photo('3.jpg')],
|
|
348
|
+
[photos[0].id],
|
|
349
|
+
as('kim')
|
|
350
|
+
)
|
|
351
|
+
expect(changed).toMatchObject({ caption: '고침', tags: ['봄'] })
|
|
352
|
+
expect((await doors.photos(changed, as('kim'))).map((p: any) => p.name)).toEqual(['2.jpg', '3.jpg'])
|
|
353
|
+
|
|
354
|
+
expect(await doors.deleteGalleryPost(one.id, as('kim'))).toBe(true)
|
|
355
|
+
expect(await listed('kim')).toEqual([])
|
|
356
|
+
expect(await dataSource.getRepository(attachmentBase.Attachment).count()).toBe(0)
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
describe('the menu’s count', () => {
|
|
361
|
+
it('★ counts the posts of others since one last opened the gallery', async () => {
|
|
362
|
+
await post('kim')
|
|
363
|
+
await post('lee')
|
|
364
|
+
expect((await doors.gallerySummary(as('lee'))).fresh).toBe(1)
|
|
365
|
+
await doors.visitGallery(as('lee'))
|
|
366
|
+
expect((await doors.gallerySummary(as('lee'))).fresh).toBe(0)
|
|
367
|
+
/* sqlite keeps a row's creation to the second - the next post comes in a later one. */
|
|
368
|
+
await new Promise(resolve => setTimeout(resolve, 1100))
|
|
369
|
+
await post('kim')
|
|
370
|
+
expect((await doors.gallerySummary(as('lee'))).fresh).toBe(1)
|
|
371
|
+
})
|
|
372
|
+
})
|