@things-factory/attachment-base 10.0.0-beta.57 → 10.0.0-beta.61

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.
@@ -1,6 +1,7 @@
1
1
  import { FileUpload } from 'graphql-upload/GraphQLUpload.js';
2
2
  import { Attachment } from './attachment';
3
3
  import { AttachmentPatch, NewAttachment } from './attachment-types';
4
+ import { ThumbnailBackfillResult } from './thumbnail-backfill';
4
5
  export declare class AttachmentMutation {
5
6
  createAttachment(attachment: NewAttachment, context: ResolverContext): Promise<Attachment>;
6
7
  createAttachments(attachments: NewAttachment[], context: ResolverContext): Promise<Attachment[]>;
@@ -16,6 +17,7 @@ export declare class AttachmentMutation {
16
17
  };
17
18
  }>;
18
19
  importAttachments(file: FileUpload, context: ResolverContext): Promise<Attachment[]>;
20
+ backfillAttachmentThumbnails(limit: number, context: ResolverContext): Promise<ThumbnailBackfillResult>;
19
21
  }
20
22
  export declare function createAttachment(_: any, { attachment }: {
21
23
  attachment: any;
@@ -21,6 +21,8 @@ const shell_1 = require("@things-factory/shell");
21
21
  const attachment_const_1 = require("../../attachment-const");
22
22
  const attachment_1 = require("./attachment");
23
23
  const attachment_types_1 = require("./attachment-types");
24
+ const thumbnail_1 = require("./thumbnail");
25
+ const thumbnail_backfill_1 = require("./thumbnail-backfill");
24
26
  const allowedMimeTypes = env_1.config.get('fileUpload/mimeTypes', []);
25
27
  let AttachmentMutation = class AttachmentMutation {
26
28
  async createAttachment(attachment, context) {
@@ -60,6 +62,9 @@ let AttachmentMutation = class AttachmentMutation {
60
62
  async importAttachments(file, context) {
61
63
  return await importAttachments(file, context);
62
64
  }
65
+ async backfillAttachmentThumbnails(limit, context) {
66
+ return await (0, thumbnail_backfill_1.backfillAttachmentThumbnails)({ limit }, context);
67
+ }
63
68
  };
64
69
  exports.AttachmentMutation = AttachmentMutation;
65
70
  tslib_1.__decorate([
@@ -153,6 +158,18 @@ tslib_1.__decorate([
153
158
  tslib_1.__metadata("design:paramtypes", [typeof (_b = typeof GraphQLUpload_js_1.FileUpload !== "undefined" && GraphQLUpload_js_1.FileUpload) === "function" ? _b : Object, Object]),
154
159
  tslib_1.__metadata("design:returntype", Promise)
155
160
  ], AttachmentMutation.prototype, "importAttachments", null);
161
+ tslib_1.__decorate([
162
+ (0, type_graphql_1.Directive)('@privilege(category: "attachment", privilege: "mutation", domainOwnerGranted: true)'),
163
+ (0, type_graphql_1.Mutation)(returns => thumbnail_backfill_1.ThumbnailBackfillResult, {
164
+ description: '썸네일이 없는 기존 첨부파일들에 대해 서버에서 썸네일을 일괄 생성한다. ' +
165
+ '한 호출당 limit 개까지만 처리하며, remaining > 0 이면 반복 호출 필요.'
166
+ }),
167
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('limit', type => type_graphql_1.Int, { nullable: true, defaultValue: 20 })),
168
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
169
+ tslib_1.__metadata("design:type", Function),
170
+ tslib_1.__metadata("design:paramtypes", [Number, Object]),
171
+ tslib_1.__metadata("design:returntype", Promise)
172
+ ], AttachmentMutation.prototype, "backfillAttachmentThumbnails", null);
156
173
  exports.AttachmentMutation = AttachmentMutation = tslib_1.__decorate([
157
174
  (0, type_graphql_1.Resolver)(attachment_1.Attachment)
158
175
  ], AttachmentMutation);
@@ -171,6 +188,7 @@ async function createAttachment(_, { attachment }, context) {
171
188
  }
172
189
  const { id, path, size, filename, encoding, contents } = await attachment_const_1.STORAGE.uploadFile({ file, context });
173
190
  const { domain, user, tx } = context.state;
191
+ const thumbnail = await (0, thumbnail_1.generateThumbnail)(contents, mimetype);
174
192
  const repository = tx ? tx.getRepository(attachment_1.Attachment) : (0, shell_1.getRepository)(attachment_1.Attachment);
175
193
  return await repository.save({
176
194
  domain,
@@ -186,7 +204,8 @@ async function createAttachment(_, { attachment }, context) {
186
204
  category: category || mimetype.split('/').shift(),
187
205
  size: size,
188
206
  path,
189
- contents
207
+ contents,
208
+ thumbnail
190
209
  });
191
210
  }
192
211
  async function createAttachments(_, { attachments }, context) {
@@ -1 +1 @@
1
- {"version":3,"file":"attachment-mutation.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment-mutation.ts"],"names":[],"mappings":";;;;AAsHA,4CAuCC;AAED,8CAYC;AAED,4CAeC;AAMD,wDAmCC;AAED,8CAMC;AAED,oCAEC;AAED,wCAEC;AAkDD,8CAwDC;;AA/VD,sEAA4D;AAC5D,+FAA2D;AAC3D,wEAAsC;AAEtC,+CAAsE;AACtE,qCAA4B;AAE5B,6CAAoD;AACpD,iDAAqD;AAErD,6DAAgD;AAChD,6CAAyC;AACzC,yDAA8E;AAE9E,MAAM,gBAAgB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAA;AAGxD,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAIvB,AAAN,KAAK,CAAC,gBAAgB,CACsB,UAAyB,EAC5D,OAAwB;QAE/B,OAAO,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,CAAA;IAC9D,CAAC;IAKK,AAAN,KAAK,CAAC,iBAAiB,CACwB,WAA4B,EAClE,OAAwB;QAE/B,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,CAAA;IAChE,CAAC;IAKK,AAAN,KAAK,CAAC,gBAAgB,CACT,EAAU,EACkB,KAAsB,EACtD,OAAwB;QAE/B,MAAM,UAAU,GAAG,MAAM,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAC,OAAO,CAAC;YACzD,KAAK,EAAE;gBACL,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE;gBACvC,EAAE;aACH;SACF,CAAC,CAAA;QAEF,OAAO,MAAM,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAC,IAAI,CAAC;YAC1C,GAAG,UAAU;YACb,GAAG,KAAK;YACR,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI;SAC5B,CAAC,CAAA;IACJ,CAAC;IAKK,AAAN,KAAK,CAAC,gBAAgB,CAAY,EAAU,EAAS,OAAwB;QAC3E,OAAO,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IACtD,CAAC;IAKK,AAAN,KAAK,CAAC,sBAAsB,CACO,MAAgB,EACG,OAAe,EAC5D,OAAwB;QAE/B,OAAO,MAAM,sBAAsB,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,CAAA;IACzE,CAAC;IAKK,AAAN,KAAK,CAAC,YAAY,CACoB,IAAgB,EAC7C,OAAwB;QAE/B,OAAO,MAAM,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;IACpD,CAAC;IAKK,AAAN,KAAK,CAAC,cAAc,CACqB,KAAmB,EACnD,OAAwB;QAE/B,OAAO,MAAM,cAAc,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,CAAA;IACvD,CAAC;IAIK,AAAN,KAAK,CAAC,iBAAiB,CACQ,IAAY,EAClC,OAAwB;QAE/B,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;IACzD,CAAC;IAKK,AAAN,KAAK,CAAC,iBAAiB,CACa,IAAgB,EAC3C,OAAwB;QAE/B,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAC/C,CAAC;CACF,CAAA;AAnGY,gDAAkB;AAIvB;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,uBAAU,CAAC;IAE7B,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,gCAAa,CAAC,CAAA;IACxC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CADgD,gCAAa;;0DAIpE;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,uBAAU,CAAC,CAAC;IAE/B,mBAAA,IAAA,kBAAG,EAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,gCAAa,CAAC,CAAC,CAAA;IAC3C,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;2DAGP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,uBAAU,CAAC;IAE7B,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IACT,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kCAAe,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;qDADwC,kCAAe;;0DAe9D;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC;IACL,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;0DAEnD;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC;IAE1B,mBAAA,IAAA,kBAAG,EAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IAC/B,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClD,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;gEAGP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,uBAAU,CAAC;IAE7B,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,0BAAa,CAAC,CAAA;IAClC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;iEADoC,6BAAU,oBAAV,6BAAU;;sDAIrD;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,uBAAU,CAAC,CAAC;IAE/B,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,0BAAa,CAAC,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;wDAGP;AAIK;IAFL,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,4BAAS,CAAC;IAC9B,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAE9F,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;IAC3B,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;2DAGP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,uBAAU,CAAC,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAE9E,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,GAAG,EAAE,CAAC,0BAAa,CAAC,CAAA;IAChC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;iEADkC,6BAAU,oBAAV,6BAAU;;2DAInD;6BAlGU,kBAAkB;IAD9B,IAAA,uBAAQ,EAAC,uBAAU,CAAC;GACR,kBAAkB,CAmG9B;AAEM,KAAK,UAAU,gBAAgB,CAAC,CAAM,EAAE,EAAE,UAAU,EAAE,EAAE,OAAwB;IACrF,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,UAAU,CAAA;IACvE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,IAAI,CAAe,CAAA;IAE/C,IAAI,gBAAgB,YAAY,KAAK,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1G,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAC7C,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC3C,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC/C,OAAO,CACL,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,KAAK,GAAG,IAAI,OAAO,KAAK,GAAG,CAAC,CAC7G,CAAA;QACH,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wCAAwC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;IAED,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,0BAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IACpG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAEhF,OAAO,MAAM,UAAU,CAAC,IAAI,CAAC;QAC3B,MAAM;QACN,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI;QACb,EAAE;QACF,WAAW;QACX,IAAI,EAAE,QAAQ;QACd,QAAQ;QACR,QAAQ;QACR,OAAO;QACP,KAAK;QACL,QAAQ,EAAE,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE;QACjD,IAAI,EAAE,IAAW;QACjB,IAAI;QACJ,QAAQ;KACT,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,CAAM,EAAE,EAAE,WAAW,EAAE,EAAE,OAAwB;IACvF,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,sBAAW,CAAC,GAAG,CAC/C,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC,CAC5E,CAAA;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,YAAM,CAAC,KAAK,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC,CAAA;QAE1E,OAAO,MAAM,CAAA;IACf,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAEM,KAAK,UAAU,gBAAgB,CAAC,CAAM,EAAE,EAAE,EAAE,EAAE,EAAE,OAAwB;IAC7E,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEpC,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAChF,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC1C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;KACzC,CAAC,CAAA;IAEF,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAA;QAC9C,MAAM,0BAAO,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QACzC,OAAO,IAAI,CAAA;IACb,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAMM,KAAK,UAAU,sBAAsB,CAC1C,CAAM,EACN,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,EAA2B,EACnD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACpC,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAChF,MAAM,iBAAiB,GAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAA,YAAE,EAAC,MAAM,CAAC,EAAE,CAAA;IAC/E,MAAM,iBAAiB,GAAQ,EAAE,KAAK,EAAE,IAAA,YAAE,EAAC,MAAM,CAAC,EAAE,CAAA;IAEpD,4BAA4B;IAC5B,IAAI,OAAO,EAAE,CAAC;QACZ,iBAAiB,CAAC,OAAO,GAAG,OAAO,CAAA;QACnC,iBAAiB,CAAC,OAAO,GAAG,OAAO,CAAA;IACrC,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACxC,KAAK,EAAE,iBAAiB;KACzB,CAAC,CAAA;IAEF,6BAA6B;IAC7B,MAAM,UAAU,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAA;IAE1C,sCAAsC;IACtC,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,GAAG,CACf,WAAW,CAAC,GAAG,CAAC,KAAK,EAAC,UAAU,EAAC,EAAE;YACjC,MAAM,0BAAO,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAC3C,CAAC,CAAC,CACH,CAAA;QAED,OAAO,IAAI,CAAA;IACb,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,iBAAiB,CACrC,CAAM,EACN,EAAE,IAAI,EAAE,EACR,OAAwB;IAExB,OAAO,MAAM,0BAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;AAC9C,CAAC;AAEM,KAAK,UAAU,YAAY,CAAC,CAAM,EAAE,EAAE,IAAI,EAAE,EAAE,OAAwB;IAC3E,OAAO,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AACxE,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,CAAM,EAAE,EAAE,KAAK,EAAE,EAAE,OAAwB;IAC9E,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AAC3E,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,YAAwB;IACnD,IAAI,EAAE,gBAAgB,EAAE,GAAG,MAAM,YAAY,CAAA;IAE7C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAiB,EAAE,CAAA;QAE/B,gBAAgB,EAAE;aACf,EAAE,CAAC,MAAM,EAAE,CAAC,KAAiB,EAAE,EAAE;YAChC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC,CAAC;aACD,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACd,IAAI,CAAC;gBACH,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;gBAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;gBACzC,OAAO,CAAC,QAAQ,CAAC,CAAA;YACnB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,CAAA;YACf,CAAC;QACH,CAAC,CAAC;aACD,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YAC5B,MAAM,CAAC,KAAK,CAAC,CAAA;QACf,CAAC,CAAC,CAAA;IACN,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAgB;IAC9E,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACzC,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACrC,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;IAEhD,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,QAAQ;QAClB,QAAQ,EAAE,QAAQ;QAClB,gBAAgB,EAAE,GAAG,EAAE;YACrB,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;YAChC,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAA;YACtC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACrB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACnB,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,MAAkB,EAAE,OAAwB;IAClF,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElD,MAAM,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAA;IAE/C,MAAM,WAAW,GAAG,EAAE,CAAA;IAEtB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,CAAA;IAE1C,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;QAC9E,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,MAAM,mCAAmC,CAAA;QAC3C,CAAC;QAED,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;QAEzD,IAAI,gBAAgB,EAAE,CAAC;YACrB,IAAI,gBAAgB,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;gBAC3C,MAAM,2BAA2B,EAAE,IAAI,IAAI,sCAAsC,CAAA;YACnF,CAAC;YAED,0BAA0B;YAC1B,SAAQ;QACV,CAAC;QAED,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;QAE1D,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,0BAAO,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAE9E,WAAW,CAAC,IAAI,CACd,MAAM,UAAU,CAAC,IAAI,CAAC;YACpB,MAAM;YACN,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,EAAE;YACF,WAAW;YACX,IAAI;YACJ,QAAQ;YACR,QAAQ;YACR,QAAQ,EAAE,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE;YACjD,IAAI,EAAE,IAAW;YACjB,IAAI;YACJ,QAAQ;SACT,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM;QACJ,MAAM,CAAC;YACL,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,GAAG,WAAW,CAAC,MAAM,6BAA6B;YACzD,IAAI,EAAE,GAAG,WAAW,CAAC,MAAM,kCAAkC,IAAI,CAAC,IAAI,EAAE;SACzE,CAAC,CAAA;IAEJ,OAAO,WAAW,CAAA;AACpB,CAAC","sourcesContent":["import { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport promisesAll from 'promises-all'\n\nimport { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { STORAGE } from '../../attachment-const'\nimport { Attachment } from './attachment'\nimport { AttachmentPatch, NewAttachment, UploadURL } from './attachment-types'\n\nconst allowedMimeTypes = config.get('fileUpload/mimeTypes', [])\n\n@Resolver(Attachment)\nexport class AttachmentMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async createAttachment(\n @Arg('attachment', type => NewAttachment) attachment: NewAttachment,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await createAttachment(null, { attachment }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async createAttachments(\n @Arg('attachments', type => [NewAttachment]) attachments: NewAttachment[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await createAttachments(null, { attachments }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async updateAttachment(\n @Arg('id') id: string,\n @Arg('patch', type => AttachmentPatch) patch: AttachmentPatch,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n const attachment = await getRepository(Attachment).findOne({\n where: {\n domain: { id: context.state.domain.id },\n id\n }\n })\n\n return await getRepository(Attachment).save({\n ...attachment,\n ...patch,\n updater: context.state.user\n })\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachment(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n return await deleteAttachment(null, { id }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachmentsByRef(\n @Arg('refBys', type => [String]) refBys: string[],\n @Arg('refType', type => String, { nullable: true }) refType: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n return await deleteAttachmentsByRef(null, { refBys, refType }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async singleUpload(\n @Arg('file', type => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await singleUpload(null, { file }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async multipleUpload(\n @Arg('files', type => [GraphQLUpload]) files: FileUpload[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await multipleUpload(null, { files }, context)\n }\n\n @Mutation(returns => UploadURL)\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n async generateUploadURL(\n @Arg('type', type => String) type: string,\n @Ctx() context: ResolverContext\n ): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await generateUploadURL(null, { type }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment], { description: 'To import some Attachments' })\n async importAttachments(\n @Arg('file', () => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await importAttachments(file, context)\n }\n}\n\nexport async function createAttachment(_: any, { attachment }, context: ResolverContext): Promise<Attachment> {\n const { file, category, refType = '', refBy, description } = attachment\n const { mimetype } = (await file) as FileUpload\n\n if (allowedMimeTypes instanceof Array && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes('*/*')) {\n const isAllowed = allowedMimeTypes.some(type => {\n const [typeMain, typeSub] = type.split('/')\n const [mimeMain, mimeSub] = mimetype.split('/')\n return (\n (typeMain === mimeMain && (typeSub === '*' || typeSub === mimeSub)) || (typeMain === '*' && typeSub === '*')\n )\n })\n\n if (!isAllowed) {\n throw Error(context.t(`error.not allowed file type for upload`, { mimetype }))\n }\n }\n\n const { id, path, size, filename, encoding, contents } = await STORAGE.uploadFile({ file, context })\n const { domain, user, tx } = context.state\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n\n return await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name: filename,\n mimetype,\n encoding,\n refType,\n refBy,\n category: category || mimetype.split('/').shift(),\n size: size as any,\n path,\n contents\n })\n}\n\nexport async function createAttachments(_: any, { attachments }, context: ResolverContext): Promise<Attachment[]> {\n const { resolve, reject } = await promisesAll.all(\n attachments.map(attachment => createAttachment(_, { attachment }, context))\n )\n\n if (reject.length) {\n reject.forEach(({ name, message }) => logger.error(`${name}: ${message}`))\n\n return reject\n }\n\n return resolve\n}\n\nexport async function deleteAttachment(_: any, { id }, context: ResolverContext): Promise<boolean> {\n const { domain, tx } = context.state\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const attachment = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (attachment) {\n await repository.delete({ id: attachment.id })\n await STORAGE.deleteFile(attachment.path)\n return true\n } else {\n return false\n }\n}\n\ninterface DeleteAttachmentsObject {\n refBys: Array<string>\n refType?: string\n}\nexport async function deleteAttachmentsByRef(\n _: any,\n { refBys, refType = null }: DeleteAttachmentsObject,\n context: ResolverContext\n): Promise<boolean> {\n const { domain, tx } = context.state\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const inquryWhereClause: any = { domain: { id: domain.id }, refBy: In(refBys) }\n const deleteWhereClause: any = { refBy: In(refBys) }\n\n // refType이 존재하면 where 절에 추가\n if (refType) {\n inquryWhereClause.refType = refType\n deleteWhereClause.refType = refType\n }\n\n const attachments = await repository.find({\n where: inquryWhereClause\n })\n\n //remove attachment from repo\n await repository.delete(deleteWhereClause)\n\n //remove files from attachments folder\n if (attachments.length) {\n await Promise.all(\n attachments.map(async attachment => {\n await STORAGE.deleteFile(attachment.path)\n })\n )\n\n return true\n } else {\n return false\n }\n}\n\nexport async function generateUploadURL(\n _: any,\n { type },\n context: ResolverContext\n): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await STORAGE.generateUploadURL(type)\n}\n\nexport async function singleUpload(_: any, { file }, context: ResolverContext): Promise<Attachment> {\n return await createAttachment(null, { attachment: { file } }, context)\n}\n\nexport async function multipleUpload(_: any, { files }, context: ResolverContext): Promise<Attachment[]> {\n return await createAttachments(null, { attachments: { files } }, context)\n}\n\nasync function parseJSONFile(uploadedFile: FileUpload): Promise<any> {\n var { createReadStream } = await uploadedFile\n\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = []\n\n createReadStream()\n .on('data', (chunk: Uint8Array) => {\n chunks.push(chunk)\n })\n .on('end', () => {\n try {\n const fileContents = Buffer.concat(chunks).toString('utf-8')\n const jsonData = JSON.parse(fileContents)\n resolve(jsonData)\n } catch (error) {\n reject(error)\n }\n })\n .on('error', (error: Error) => {\n reject(error)\n })\n })\n}\n\nfunction dataURLToFileUpload(dataURL: string, filename: string, mimeType: string): FileUpload {\n const indexOfComma = dataURL.indexOf(',')\n if (indexOfComma === -1) {\n throw new Error('Invalid Data URL')\n }\n\n const base64Data = dataURL.slice(indexOfComma + 1)\n const buffer = Buffer.from(base64Data, 'base64')\n\n return {\n filename,\n mimetype: mimeType,\n encoding: 'base64',\n createReadStream: () => {\n const stream = require('stream')\n const readable = new stream.Readable()\n readable.push(buffer)\n readable.push(null)\n return readable\n }\n }\n}\n\nexport async function importAttachments(upload: FileUpload, context: ResolverContext): Promise<Attachment[]> {\n const { domain, user, notify, tx } = context.state\n\n const repository = tx.getRepository(Attachment)\n\n const attachments = []\n\n const parsed = await parseJSONFile(upload)\n\n for (const id in parsed) {\n var { name, description, category, mimetype, encoding, contents } = parsed[id]\n if (!name || !contents || !mimetype) {\n throw 'Malformed attachments import file'\n }\n\n var sameIdAttachment = await repository.findOneBy({ id })\n\n if (sameIdAttachment) {\n if (sameIdAttachment.domainId != domain.id) {\n throw `Attachment with id/name(${id}/${name}) is already taken in another domain`\n }\n\n // 동일 아이디 첨부파일이 있다면, 스킵한다.\n continue\n }\n\n const file = dataURLToFileUpload(contents, name, mimetype)\n\n var { path, size, contents } = await STORAGE.uploadFile({ id, file, context })\n\n attachments.push(\n await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name,\n mimetype,\n encoding,\n category: category || mimetype.split('/').shift(),\n size: size as any,\n path,\n contents\n })\n )\n }\n\n notify &&\n notify({\n mode: 'in-app',\n title: `${attachments.length} Attachment(s) are imported`,\n body: `${attachments.length} Attachment(s) are imported by ${user.name}`\n })\n\n return attachments\n}\n"]}
1
+ {"version":3,"file":"attachment-mutation.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment-mutation.ts"],"names":[],"mappings":";;;;AAqIA,4CA0CC;AAED,8CAYC;AAED,4CAeC;AAMD,wDAmCC;AAED,8CAMC;AAED,oCAEC;AAED,wCAEC;AAkDD,8CAwDC;;AAjXD,sEAA4D;AAC5D,+FAA2D;AAC3D,wEAAsC;AAEtC,+CAA2E;AAC3E,qCAA4B;AAE5B,6CAAoD;AACpD,iDAAqD;AAErD,6DAAgD;AAChD,6CAAyC;AACzC,yDAA8E;AAC9E,2CAA+C;AAC/C,6DAA4F;AAE5F,MAAM,gBAAgB,GAAG,YAAM,CAAC,GAAG,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAA;AAGxD,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAIvB,AAAN,KAAK,CAAC,gBAAgB,CACsB,UAAyB,EAC5D,OAAwB;QAE/B,OAAO,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,CAAA;IAC9D,CAAC;IAKK,AAAN,KAAK,CAAC,iBAAiB,CACwB,WAA4B,EAClE,OAAwB;QAE/B,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,OAAO,CAAC,CAAA;IAChE,CAAC;IAKK,AAAN,KAAK,CAAC,gBAAgB,CACT,EAAU,EACkB,KAAsB,EACtD,OAAwB;QAE/B,MAAM,UAAU,GAAG,MAAM,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAC,OAAO,CAAC;YACzD,KAAK,EAAE;gBACL,MAAM,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE;gBACvC,EAAE;aACH;SACF,CAAC,CAAA;QAEF,OAAO,MAAM,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAC,IAAI,CAAC;YAC1C,GAAG,UAAU;YACb,GAAG,KAAK;YACR,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI;SAC5B,CAAC,CAAA;IACJ,CAAC;IAKK,AAAN,KAAK,CAAC,gBAAgB,CAAY,EAAU,EAAS,OAAwB;QAC3E,OAAO,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;IACtD,CAAC;IAKK,AAAN,KAAK,CAAC,sBAAsB,CACO,MAAgB,EACG,OAAe,EAC5D,OAAwB;QAE/B,OAAO,MAAM,sBAAsB,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,OAAO,CAAC,CAAA;IACzE,CAAC;IAKK,AAAN,KAAK,CAAC,YAAY,CACoB,IAAgB,EAC7C,OAAwB;QAE/B,OAAO,MAAM,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;IACpD,CAAC;IAKK,AAAN,KAAK,CAAC,cAAc,CACqB,KAAmB,EACnD,OAAwB;QAE/B,OAAO,MAAM,cAAc,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,CAAA;IACvD,CAAC;IAIK,AAAN,KAAK,CAAC,iBAAiB,CACQ,IAAY,EAClC,OAAwB;QAE/B,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;IACzD,CAAC;IAKK,AAAN,KAAK,CAAC,iBAAiB,CACa,IAAgB,EAC3C,OAAwB;QAE/B,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAC/C,CAAC;IAQK,AAAN,KAAK,CAAC,4BAA4B,CACiC,KAAa,EACvE,OAAwB;QAE/B,OAAO,MAAM,IAAA,iDAA4B,EAAC,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,CAAA;IAC/D,CAAC;CACF,CAAA;AAhHY,gDAAkB;AAIvB;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,uBAAU,CAAC;IAE7B,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,gCAAa,CAAC,CAAA;IACxC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;6CADgD,gCAAa;;0DAIpE;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,uBAAU,CAAC,CAAC;IAE/B,mBAAA,IAAA,kBAAG,EAAC,aAAa,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,gCAAa,CAAC,CAAC,CAAA;IAC3C,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;2DAGP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,uBAAU,CAAC;IAE7B,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IACT,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kCAAe,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;qDADwC,kCAAe;;0DAe9D;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC;IACL,mBAAA,IAAA,kBAAG,EAAC,IAAI,CAAC,CAAA;IAAc,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;0DAEnD;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC;IAE1B,mBAAA,IAAA,kBAAG,EAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IAC/B,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClD,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;gEAGP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,uBAAU,CAAC;IAE7B,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,0BAAa,CAAC,CAAA;IAClC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;iEADoC,6BAAU,oBAAV,6BAAU;;sDAIrD;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,uBAAU,CAAC,CAAC;IAE/B,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,0BAAa,CAAC,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;wDAGP;AAIK;IAFL,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,4BAAS,CAAC;IAC9B,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAE9F,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;IAC3B,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;2DAGP;AAKK;IAHL,IAAA,wBAAS,EAAC,cAAc,CAAC;IACzB,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,uBAAU,CAAC,EAAE,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;IAE9E,mBAAA,IAAA,kBAAG,EAAC,MAAM,EAAE,GAAG,EAAE,CAAC,0BAAa,CAAC,CAAA;IAChC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;iEADkC,6BAAU,oBAAV,6BAAU;;2DAInD;AAQK;IANL,IAAA,wBAAS,EAAC,qFAAqF,CAAC;IAChG,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,4CAAuB,EAAE;QAC5C,WAAW,EACT,0CAA0C;YAC1C,mDAAmD;KACtD,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAA;IAC/D,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;sEAGP;6BA/GU,kBAAkB;IAD9B,IAAA,uBAAQ,EAAC,uBAAU,CAAC;GACR,kBAAkB,CAgH9B;AAEM,KAAK,UAAU,gBAAgB,CAAC,CAAM,EAAE,EAAE,UAAU,EAAE,EAAE,OAAwB;IACrF,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAG,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,UAAU,CAAA;IACvE,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,IAAI,CAAe,CAAA;IAE/C,IAAI,gBAAgB,YAAY,KAAK,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1G,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAC7C,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC3C,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC/C,OAAO,CACL,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,KAAK,GAAG,IAAI,OAAO,KAAK,GAAG,CAAC,CAC7G,CAAA;QACH,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wCAAwC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;IAED,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,0BAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;IACpG,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAE1C,MAAM,SAAS,GAAG,MAAM,IAAA,6BAAiB,EAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAE7D,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAEhF,OAAO,MAAM,UAAU,CAAC,IAAI,CAAC;QAC3B,MAAM;QACN,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI;QACb,EAAE;QACF,WAAW;QACX,IAAI,EAAE,QAAQ;QACd,QAAQ;QACR,QAAQ;QACR,OAAO;QACP,KAAK;QACL,QAAQ,EAAE,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE;QACjD,IAAI,EAAE,IAAW;QACjB,IAAI;QACJ,QAAQ;QACR,SAAS;KACV,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,CAAM,EAAE,EAAE,WAAW,EAAE,EAAE,OAAwB;IACvF,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,sBAAW,CAAC,GAAG,CAC/C,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC,CAC5E,CAAA;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,YAAM,CAAC,KAAK,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CAAC,CAAC,CAAA;QAE1E,OAAO,MAAM,CAAA;IACf,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAEM,KAAK,UAAU,gBAAgB,CAAC,CAAM,EAAE,EAAE,EAAE,EAAE,EAAE,OAAwB;IAC7E,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAEpC,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAChF,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;QAC1C,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE;KACzC,CAAC,CAAA;IAEF,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAA;QAC9C,MAAM,0BAAO,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QACzC,OAAO,IAAI,CAAA;IACb,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAMM,KAAK,UAAU,sBAAsB,CAC1C,CAAM,EACN,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,EAA2B,EACnD,OAAwB;IAExB,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACpC,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAC,CAAC,CAAC,IAAA,qBAAa,EAAC,uBAAU,CAAC,CAAA;IAChF,MAAM,iBAAiB,GAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAA,YAAE,EAAC,MAAM,CAAC,EAAE,CAAA;IAC/E,MAAM,iBAAiB,GAAQ,EAAE,KAAK,EAAE,IAAA,YAAE,EAAC,MAAM,CAAC,EAAE,CAAA;IAEpD,4BAA4B;IAC5B,IAAI,OAAO,EAAE,CAAC;QACZ,iBAAiB,CAAC,OAAO,GAAG,OAAO,CAAA;QACnC,iBAAiB,CAAC,OAAO,GAAG,OAAO,CAAA;IACrC,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC;QACxC,KAAK,EAAE,iBAAiB;KACzB,CAAC,CAAA;IAEF,6BAA6B;IAC7B,MAAM,UAAU,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAA;IAE1C,sCAAsC;IACtC,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,OAAO,CAAC,GAAG,CACf,WAAW,CAAC,GAAG,CAAC,KAAK,EAAC,UAAU,EAAC,EAAE;YACjC,MAAM,0BAAO,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;QAC3C,CAAC,CAAC,CACH,CAAA;QAED,OAAO,IAAI,CAAA;IACb,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,iBAAiB,CACrC,CAAM,EACN,EAAE,IAAI,EAAE,EACR,OAAwB;IAExB,OAAO,MAAM,0BAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;AAC9C,CAAC;AAEM,KAAK,UAAU,YAAY,CAAC,CAAM,EAAE,EAAE,IAAI,EAAE,EAAE,OAAwB;IAC3E,OAAO,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AACxE,CAAC;AAEM,KAAK,UAAU,cAAc,CAAC,CAAM,EAAE,EAAE,KAAK,EAAE,EAAE,OAAwB;IAC9E,OAAO,MAAM,iBAAiB,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC,CAAA;AAC3E,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,YAAwB;IACnD,IAAI,EAAE,gBAAgB,EAAE,GAAG,MAAM,YAAY,CAAA;IAE7C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAiB,EAAE,CAAA;QAE/B,gBAAgB,EAAE;aACf,EAAE,CAAC,MAAM,EAAE,CAAC,KAAiB,EAAE,EAAE;YAChC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC,CAAC;aACD,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACd,IAAI,CAAC;gBACH,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;gBAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAA;gBACzC,OAAO,CAAC,QAAQ,CAAC,CAAA;YACnB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,CAAA;YACf,CAAC;QACH,CAAC,CAAC;aACD,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YAC5B,MAAM,CAAC,KAAK,CAAC,CAAA;QACf,CAAC,CAAC,CAAA;IACN,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,mBAAmB,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAgB;IAC9E,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACzC,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACrC,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAA;IAClD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;IAEhD,OAAO;QACL,QAAQ;QACR,QAAQ,EAAE,QAAQ;QAClB,QAAQ,EAAE,QAAQ;QAClB,gBAAgB,EAAE,GAAG,EAAE;YACrB,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;YAChC,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAA;YACtC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACrB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACnB,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,MAAkB,EAAE,OAAwB;IAClF,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAElD,MAAM,UAAU,GAAG,EAAE,CAAC,aAAa,CAAC,uBAAU,CAAC,CAAA;IAE/C,MAAM,WAAW,GAAG,EAAE,CAAA;IAEtB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,CAAA;IAE1C,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;QAC9E,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,MAAM,mCAAmC,CAAA;QAC3C,CAAC;QAED,IAAI,gBAAgB,GAAG,MAAM,UAAU,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;QAEzD,IAAI,gBAAgB,EAAE,CAAC;YACrB,IAAI,gBAAgB,CAAC,QAAQ,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;gBAC3C,MAAM,2BAA2B,EAAE,IAAI,IAAI,sCAAsC,CAAA;YACnF,CAAC;YAED,0BAA0B;YAC1B,SAAQ;QACV,CAAC;QAED,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;QAE1D,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,0BAAO,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAE9E,WAAW,CAAC,IAAI,CACd,MAAM,UAAU,CAAC,IAAI,CAAC;YACpB,MAAM;YACN,OAAO,EAAE,IAAI;YACb,OAAO,EAAE,IAAI;YACb,EAAE;YACF,WAAW;YACX,IAAI;YACJ,QAAQ;YACR,QAAQ;YACR,QAAQ,EAAE,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE;YACjD,IAAI,EAAE,IAAW;YACjB,IAAI;YACJ,QAAQ;SACT,CAAC,CACH,CAAA;IACH,CAAC;IAED,MAAM;QACJ,MAAM,CAAC;YACL,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,GAAG,WAAW,CAAC,MAAM,6BAA6B;YACzD,IAAI,EAAE,GAAG,WAAW,CAAC,MAAM,kCAAkC,IAAI,CAAC,IAAI,EAAE;SACzE,CAAC,CAAA;IAEJ,OAAO,WAAW,CAAA;AACpB,CAAC","sourcesContent":["import { FileUpload } from 'graphql-upload/GraphQLUpload.js'\nimport GraphQLUpload from 'graphql-upload/GraphQLUpload.js'\nimport promisesAll from 'promises-all'\n\nimport { Arg, Ctx, Directive, Int, Mutation, Resolver } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { config, logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { STORAGE } from '../../attachment-const'\nimport { Attachment } from './attachment'\nimport { AttachmentPatch, NewAttachment, UploadURL } from './attachment-types'\nimport { generateThumbnail } from './thumbnail'\nimport { backfillAttachmentThumbnails, ThumbnailBackfillResult } from './thumbnail-backfill'\n\nconst allowedMimeTypes = config.get('fileUpload/mimeTypes', [])\n\n@Resolver(Attachment)\nexport class AttachmentMutation {\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async createAttachment(\n @Arg('attachment', type => NewAttachment) attachment: NewAttachment,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await createAttachment(null, { attachment }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async createAttachments(\n @Arg('attachments', type => [NewAttachment]) attachments: NewAttachment[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await createAttachments(null, { attachments }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async updateAttachment(\n @Arg('id') id: string,\n @Arg('patch', type => AttachmentPatch) patch: AttachmentPatch,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n const attachment = await getRepository(Attachment).findOne({\n where: {\n domain: { id: context.state.domain.id },\n id\n }\n })\n\n return await getRepository(Attachment).save({\n ...attachment,\n ...patch,\n updater: context.state.user\n })\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachment(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {\n return await deleteAttachment(null, { id }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean)\n async deleteAttachmentsByRef(\n @Arg('refBys', type => [String]) refBys: string[],\n @Arg('refType', type => String, { nullable: true }) refType: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n return await deleteAttachmentsByRef(null, { refBys, refType }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Attachment)\n async singleUpload(\n @Arg('file', type => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment> {\n return await singleUpload(null, { file }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment])\n async multipleUpload(\n @Arg('files', type => [GraphQLUpload]) files: FileUpload[],\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await multipleUpload(null, { files }, context)\n }\n\n @Mutation(returns => UploadURL)\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n async generateUploadURL(\n @Arg('type', type => String) type: string,\n @Ctx() context: ResolverContext\n ): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await generateUploadURL(null, { type }, context)\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => [Attachment], { description: 'To import some Attachments' })\n async importAttachments(\n @Arg('file', () => GraphQLUpload) file: FileUpload,\n @Ctx() context: ResolverContext\n ): Promise<Attachment[]> {\n return await importAttachments(file, context)\n }\n\n @Directive('@privilege(category: \"attachment\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => ThumbnailBackfillResult, {\n description:\n '썸네일이 없는 기존 첨부파일들에 대해 서버에서 썸네일을 일괄 생성한다. ' +\n '한 호출당 limit 개까지만 처리하며, remaining > 0 이면 반복 호출 필요.'\n })\n async backfillAttachmentThumbnails(\n @Arg('limit', type => Int, { nullable: true, defaultValue: 20 }) limit: number,\n @Ctx() context: ResolverContext\n ): Promise<ThumbnailBackfillResult> {\n return await backfillAttachmentThumbnails({ limit }, context)\n }\n}\n\nexport async function createAttachment(_: any, { attachment }, context: ResolverContext): Promise<Attachment> {\n const { file, category, refType = '', refBy, description } = attachment\n const { mimetype } = (await file) as FileUpload\n\n if (allowedMimeTypes instanceof Array && allowedMimeTypes.length > 0 && !allowedMimeTypes.includes('*/*')) {\n const isAllowed = allowedMimeTypes.some(type => {\n const [typeMain, typeSub] = type.split('/')\n const [mimeMain, mimeSub] = mimetype.split('/')\n return (\n (typeMain === mimeMain && (typeSub === '*' || typeSub === mimeSub)) || (typeMain === '*' && typeSub === '*')\n )\n })\n\n if (!isAllowed) {\n throw Error(context.t(`error.not allowed file type for upload`, { mimetype }))\n }\n }\n\n const { id, path, size, filename, encoding, contents } = await STORAGE.uploadFile({ file, context })\n const { domain, user, tx } = context.state\n\n const thumbnail = await generateThumbnail(contents, mimetype)\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n\n return await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name: filename,\n mimetype,\n encoding,\n refType,\n refBy,\n category: category || mimetype.split('/').shift(),\n size: size as any,\n path,\n contents,\n thumbnail\n })\n}\n\nexport async function createAttachments(_: any, { attachments }, context: ResolverContext): Promise<Attachment[]> {\n const { resolve, reject } = await promisesAll.all(\n attachments.map(attachment => createAttachment(_, { attachment }, context))\n )\n\n if (reject.length) {\n reject.forEach(({ name, message }) => logger.error(`${name}: ${message}`))\n\n return reject\n }\n\n return resolve\n}\n\nexport async function deleteAttachment(_: any, { id }, context: ResolverContext): Promise<boolean> {\n const { domain, tx } = context.state\n\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const attachment = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (attachment) {\n await repository.delete({ id: attachment.id })\n await STORAGE.deleteFile(attachment.path)\n return true\n } else {\n return false\n }\n}\n\ninterface DeleteAttachmentsObject {\n refBys: Array<string>\n refType?: string\n}\nexport async function deleteAttachmentsByRef(\n _: any,\n { refBys, refType = null }: DeleteAttachmentsObject,\n context: ResolverContext\n): Promise<boolean> {\n const { domain, tx } = context.state\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n const inquryWhereClause: any = { domain: { id: domain.id }, refBy: In(refBys) }\n const deleteWhereClause: any = { refBy: In(refBys) }\n\n // refType이 존재하면 where 절에 추가\n if (refType) {\n inquryWhereClause.refType = refType\n deleteWhereClause.refType = refType\n }\n\n const attachments = await repository.find({\n where: inquryWhereClause\n })\n\n //remove attachment from repo\n await repository.delete(deleteWhereClause)\n\n //remove files from attachments folder\n if (attachments.length) {\n await Promise.all(\n attachments.map(async attachment => {\n await STORAGE.deleteFile(attachment.path)\n })\n )\n\n return true\n } else {\n return false\n }\n}\n\nexport async function generateUploadURL(\n _: any,\n { type },\n context: ResolverContext\n): Promise<{ url: string; fields: { [key: string]: string } }> {\n return await STORAGE.generateUploadURL(type)\n}\n\nexport async function singleUpload(_: any, { file }, context: ResolverContext): Promise<Attachment> {\n return await createAttachment(null, { attachment: { file } }, context)\n}\n\nexport async function multipleUpload(_: any, { files }, context: ResolverContext): Promise<Attachment[]> {\n return await createAttachments(null, { attachments: { files } }, context)\n}\n\nasync function parseJSONFile(uploadedFile: FileUpload): Promise<any> {\n var { createReadStream } = await uploadedFile\n\n return new Promise((resolve, reject) => {\n const chunks: Uint8Array[] = []\n\n createReadStream()\n .on('data', (chunk: Uint8Array) => {\n chunks.push(chunk)\n })\n .on('end', () => {\n try {\n const fileContents = Buffer.concat(chunks).toString('utf-8')\n const jsonData = JSON.parse(fileContents)\n resolve(jsonData)\n } catch (error) {\n reject(error)\n }\n })\n .on('error', (error: Error) => {\n reject(error)\n })\n })\n}\n\nfunction dataURLToFileUpload(dataURL: string, filename: string, mimeType: string): FileUpload {\n const indexOfComma = dataURL.indexOf(',')\n if (indexOfComma === -1) {\n throw new Error('Invalid Data URL')\n }\n\n const base64Data = dataURL.slice(indexOfComma + 1)\n const buffer = Buffer.from(base64Data, 'base64')\n\n return {\n filename,\n mimetype: mimeType,\n encoding: 'base64',\n createReadStream: () => {\n const stream = require('stream')\n const readable = new stream.Readable()\n readable.push(buffer)\n readable.push(null)\n return readable\n }\n }\n}\n\nexport async function importAttachments(upload: FileUpload, context: ResolverContext): Promise<Attachment[]> {\n const { domain, user, notify, tx } = context.state\n\n const repository = tx.getRepository(Attachment)\n\n const attachments = []\n\n const parsed = await parseJSONFile(upload)\n\n for (const id in parsed) {\n var { name, description, category, mimetype, encoding, contents } = parsed[id]\n if (!name || !contents || !mimetype) {\n throw 'Malformed attachments import file'\n }\n\n var sameIdAttachment = await repository.findOneBy({ id })\n\n if (sameIdAttachment) {\n if (sameIdAttachment.domainId != domain.id) {\n throw `Attachment with id/name(${id}/${name}) is already taken in another domain`\n }\n\n // 동일 아이디 첨부파일이 있다면, 스킵한다.\n continue\n }\n\n const file = dataURLToFileUpload(contents, name, mimetype)\n\n var { path, size, contents } = await STORAGE.uploadFile({ id, file, context })\n\n attachments.push(\n await repository.save({\n domain,\n creator: user,\n updater: user,\n id,\n description,\n name,\n mimetype,\n encoding,\n category: category || mimetype.split('/').shift(),\n size: size as any,\n path,\n contents\n })\n )\n }\n\n notify &&\n notify({\n mode: 'in-app',\n title: `${attachments.length} Attachment(s) are imported`,\n body: `${attachments.length} Attachment(s) are imported by ${user.name}`\n })\n\n return attachments\n}\n"]}
@@ -14,6 +14,13 @@ export declare class Attachment {
14
14
  path?: string;
15
15
  size?: string;
16
16
  contents?: Buffer;
17
+ /**
18
+ * 썸네일 이미지 리소스 URL. 두 가지 형태 모두 유효:
19
+ * - 내부 생성 썸네일: `data:image/jpeg;base64,...` (업로드 시 자동 생성)
20
+ * - 외부 리소스 참조: `https://cdn.example.com/thumb.jpg`
21
+ * 둘 다 `<img src>` 에 그대로 사용 가능.
22
+ */
23
+ thumbnail?: string;
17
24
  tags?: string[];
18
25
  createdAt?: Date;
19
26
  updatedAt?: Date;
@@ -93,8 +93,20 @@ tslib_1.__decorate([
93
93
  : 'blob',
94
94
  length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
95
95
  }),
96
- tslib_1.__metadata("design:type", Buffer)
96
+ tslib_1.__metadata("design:type", Buffer
97
+ /**
98
+ * 썸네일 이미지 리소스 URL. 두 가지 형태 모두 유효:
99
+ * - 내부 생성 썸네일: `data:image/jpeg;base64,...` (업로드 시 자동 생성)
100
+ * - 외부 리소스 참조: `https://cdn.example.com/thumb.jpg`
101
+ * 둘 다 `<img src>` 에 그대로 사용 가능.
102
+ */
103
+ )
97
104
  ], Attachment.prototype, "contents", void 0);
105
+ tslib_1.__decorate([
106
+ (0, typeorm_1.Column)({ type: 'text', nullable: true }),
107
+ (0, type_graphql_1.Field)({ nullable: true }),
108
+ tslib_1.__metadata("design:type", String)
109
+ ], Attachment.prototype, "thumbnail", void 0);
98
110
  tslib_1.__decorate([
99
111
  (0, typeorm_1.Column)('simple-json', { nullable: true, default: null }),
100
112
  (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true }),
@@ -1 +1 @@
1
- {"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, ObjectType } from 'type-graphql'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\nimport { ATTACHMENT_PATH } from '../../attachment-const'\nimport { config } from '@things-factory/env'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_attachment_0', (attachment: Attachment) => [attachment.domain, attachment.name], { unique: false })\n@Index('ix_attachment_1', (attachment: Attachment) => [attachment.domain, attachment.category, attachment.name], {\n unique: false\n})\n@Index('ix_attachment_2', (attachment: Attachment) => [attachment.domain, attachment.refBy], {\n unique: false\n})\n@Index('ix_attachment_3', (attachment: Attachment) => [attachment.domain, attachment.refType, attachment.refBy], {\n unique: false\n})\n@ObjectType()\nexport class Attachment {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id?: string\n\n @ManyToOne(type => Domain, { nullable: false })\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((attachment: Attachment) => attachment.domain)\n domainId?: string\n\n @Column()\n @Field()\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n @Column()\n @Field()\n mimetype?: string\n\n @Column()\n @Field()\n encoding?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n category?: string\n\n @Column({ nullable: true, default: '' })\n @Field({ nullable: true })\n refType?: string = ''\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n refBy?: string\n\n @Column()\n @Field()\n path?: string\n\n @Column({\n nullable: true,\n type: DATABASE_TYPE == 'mssql' ? 'bigint' : undefined\n })\n @Field()\n size?: string\n\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longblob'\n : DATABASE_TYPE == 'postgres'\n ? 'bytea'\n : DATABASE_TYPE == 'mssql'\n ? 'varbinary'\n : 'blob',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n contents?: Buffer\n\n @Column('simple-json', { nullable: true, default: null })\n @Field(type => ScalarObject, { nullable: true })\n tags?: string[]\n\n @CreateDateColumn()\n @Field()\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field()\n updatedAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((attachment: Attachment) => attachment.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((attachment: Attachment) => attachment.updater)\n updaterId?: string\n\n @Field()\n get fullpath(): string {\n return `/${ATTACHMENT_PATH}/${this.path}`\n }\n}\n"]}
1
+ {"version":3,"file":"attachment.js","sourceRoot":"","sources":["../../../server/service/attachment/attachment.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, ID, ObjectType } from 'type-graphql'\nimport {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\n\nimport { User } from '@things-factory/auth-base'\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\nimport { ATTACHMENT_PATH } from '../../attachment-const'\nimport { config } from '@things-factory/env'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n@Index('ix_attachment_0', (attachment: Attachment) => [attachment.domain, attachment.name], { unique: false })\n@Index('ix_attachment_1', (attachment: Attachment) => [attachment.domain, attachment.category, attachment.name], {\n unique: false\n})\n@Index('ix_attachment_2', (attachment: Attachment) => [attachment.domain, attachment.refBy], {\n unique: false\n})\n@Index('ix_attachment_3', (attachment: Attachment) => [attachment.domain, attachment.refType, attachment.refBy], {\n unique: false\n})\n@ObjectType()\nexport class Attachment {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id?: string\n\n @ManyToOne(type => Domain, { nullable: false })\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((attachment: Attachment) => attachment.domain)\n domainId?: string\n\n @Column()\n @Field()\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n description?: string\n\n @Column()\n @Field()\n mimetype?: string\n\n @Column()\n @Field()\n encoding?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n category?: string\n\n @Column({ nullable: true, default: '' })\n @Field({ nullable: true })\n refType?: string = ''\n\n @Column({ nullable: true })\n @Field({ nullable: true })\n refBy?: string\n\n @Column()\n @Field()\n path?: string\n\n @Column({\n nullable: true,\n type: DATABASE_TYPE == 'mssql' ? 'bigint' : undefined\n })\n @Field()\n size?: string\n\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longblob'\n : DATABASE_TYPE == 'postgres'\n ? 'bytea'\n : DATABASE_TYPE == 'mssql'\n ? 'varbinary'\n : 'blob',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n contents?: Buffer\n\n /**\n * 썸네일 이미지 리소스 URL. 두 가지 형태 모두 유효:\n * - 내부 생성 썸네일: `data:image/jpeg;base64,...` (업로드 시 자동 생성)\n * - 외부 리소스 참조: `https://cdn.example.com/thumb.jpg`\n * 둘 다 `<img src>` 에 그대로 사용 가능.\n */\n @Column({ type: 'text', nullable: true })\n @Field({ nullable: true })\n thumbnail?: string\n\n @Column('simple-json', { nullable: true, default: null })\n @Field(type => ScalarObject, { nullable: true })\n tags?: string[]\n\n @CreateDateColumn()\n @Field()\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field()\n updatedAt?: Date\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((attachment: Attachment) => attachment.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((attachment: Attachment) => attachment.updater)\n updaterId?: string\n\n @Field()\n get fullpath(): string {\n return `/${ATTACHMENT_PATH}/${this.path}`\n }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * GLTF/GLB 바이트로부터 정적 JPEG 썸네일 data URI 생성.
3
+ * headless Chrome 에 Three.js 로 렌더링된 단일 프레임을 캡처한다.
4
+ * 지원 타입: model/gltf-binary (.glb), model/gltf+json (.gltf)
5
+ */
6
+ export declare function generateGltfThumbnail(contents: Buffer | undefined | null, mimetype: string | undefined): Promise<string | undefined>;
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateGltfThumbnail = generateGltfThumbnail;
4
+ const env_1 = require("@things-factory/env");
5
+ const shell_1 = require("@things-factory/shell");
6
+ /** 첨부파일 썸네일 전용 headless pool — 보드 pool 과 격리 */
7
+ const POOL_NAME = 'attachment-thumbnail';
8
+ const W = 400;
9
+ const H = 400;
10
+ const TIMEOUT_MS = 15000;
11
+ /**
12
+ * 약속: 아래 virtual URL 들은 request interception 으로만 의미를 가짐.
13
+ * - /__viewer__ : 뷰어 HTML
14
+ * - /__gltf__ : GLTF 바이트 (binary) 또는 JSON (text)
15
+ * puppeteer page 가 내부 네비게이션/fetch 시 이 URL 들을 만나면 mock response.
16
+ */
17
+ const VIEWER_URL = 'http://attachment-thumbnail.local/__viewer__';
18
+ const GLTF_URL = 'http://attachment-thumbnail.local/__gltf__';
19
+ const VIEWER_HTML = `<!doctype html>
20
+ <html><head>
21
+ <meta charset="utf-8">
22
+ <style>
23
+ html, body { margin: 0; padding: 0; background: #f5f5f5; }
24
+ canvas { display: block; }
25
+ </style>
26
+ <script type="importmap">
27
+ {
28
+ "imports": {
29
+ "three": "https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.module.js",
30
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/"
31
+ }
32
+ }
33
+ </script>
34
+ </head>
35
+ <body>
36
+ <script type="module">
37
+ import * as THREE from 'three'
38
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
39
+ import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
40
+ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'
41
+ import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'
42
+
43
+ const W = ${W}, H = ${H}
44
+ window.__gltfReady = false
45
+ window.__gltfError = null
46
+
47
+ const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
48
+ renderer.setSize(W, H)
49
+ renderer.setClearColor(0xf5f5f5, 1)
50
+ renderer.outputColorSpace = THREE.SRGBColorSpace
51
+ document.body.appendChild(renderer.domElement)
52
+
53
+ const scene = new THREE.Scene()
54
+ const camera = new THREE.PerspectiveCamera(35, W / H, 0.1, 10000)
55
+
56
+ // 기본 조명 — 모델 세부 손실 최소화
57
+ scene.add(new THREE.AmbientLight(0xffffff, 0.6))
58
+ const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8)
59
+ scene.add(hemi)
60
+ const dir = new THREE.DirectionalLight(0xffffff, 1.2)
61
+ dir.position.set(5, 10, 7.5)
62
+ scene.add(dir)
63
+
64
+ ;(async () => {
65
+ try {
66
+ const res = await fetch('${GLTF_URL}')
67
+ if (!res.ok) throw new Error('fetch failed: ' + res.status)
68
+ const ab = await res.arrayBuffer()
69
+
70
+ const loader = new GLTFLoader()
71
+
72
+ // DRACO 압축 메쉬 지원 (필요한 GLB 만 로드 시 decoder fetch)
73
+ const draco = new DRACOLoader()
74
+ draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/draco/')
75
+ loader.setDRACOLoader(draco)
76
+
77
+ // KTX2 (BasisU) 압축 텍스처 지원
78
+ const ktx2 = new KTX2Loader()
79
+ ktx2.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/')
80
+ ktx2.detectSupport(renderer)
81
+ loader.setKTX2Loader(ktx2)
82
+
83
+ // Meshopt 압축 지원
84
+ loader.setMeshoptDecoder(MeshoptDecoder)
85
+
86
+ const gltf = await loader.parseAsync(ab, '')
87
+ scene.add(gltf.scene)
88
+
89
+ // Bounding box 기반 카메라 auto-fit
90
+ const box = new THREE.Box3().setFromObject(gltf.scene)
91
+ const size = box.getSize(new THREE.Vector3())
92
+ const center = box.getCenter(new THREE.Vector3())
93
+ const maxDim = Math.max(size.x, size.y, size.z) || 1
94
+ const fov = camera.fov * (Math.PI / 180)
95
+ const distance = (maxDim / 2) / Math.tan(fov / 2) * 2.0
96
+
97
+ // 45도 정면-위쪽 뷰포인트 — 3D 모델 특징이 잘 드러나는 각도
98
+ const dirVec = new THREE.Vector3(1, 0.7, 1).normalize()
99
+ camera.position.copy(center).add(dirVec.multiplyScalar(distance))
100
+ camera.near = Math.max(distance / 100, 0.01)
101
+ camera.far = distance * 10
102
+ camera.updateProjectionMatrix()
103
+ camera.lookAt(center)
104
+
105
+ renderer.render(scene, camera)
106
+
107
+ // 렌더 완료를 다음 rAF 까지 보장
108
+ requestAnimationFrame(() => {
109
+ renderer.render(scene, camera)
110
+ window.__gltfReady = true
111
+ })
112
+ } catch (e) {
113
+ window.__gltfError = e?.message || String(e)
114
+ }
115
+ })()
116
+ </script>
117
+ </body></html>`;
118
+ /**
119
+ * GLTF/GLB 바이트로부터 정적 JPEG 썸네일 data URI 생성.
120
+ * headless Chrome 에 Three.js 로 렌더링된 단일 프레임을 캡처한다.
121
+ * 지원 타입: model/gltf-binary (.glb), model/gltf+json (.gltf)
122
+ */
123
+ async function generateGltfThumbnail(contents, mimetype) {
124
+ if (!contents)
125
+ return;
126
+ if (mimetype !== 'model/gltf-binary' && mimetype !== 'model/gltf+json')
127
+ return;
128
+ let pool;
129
+ try {
130
+ pool = (0, shell_1.getOrCreateHeadlessPool)(POOL_NAME, { min: 0, max: 2 });
131
+ }
132
+ catch (e) {
133
+ env_1.logger.warn(`[attachment] gltf thumbnail — headless pool unavailable: ${e.message}`);
134
+ return;
135
+ }
136
+ const browser = await pool.acquire();
137
+ if (!browser)
138
+ return;
139
+ const page = await browser.newPage();
140
+ try {
141
+ await page.setViewport({ width: W, height: H, deviceScaleFactor: 1 });
142
+ await page.setRequestInterception(true);
143
+ page.on('request', async (req) => {
144
+ const url = req.url();
145
+ if (url === VIEWER_URL) {
146
+ await req.respond({ status: 200, contentType: 'text/html; charset=utf-8', body: VIEWER_HTML });
147
+ }
148
+ else if (url === GLTF_URL) {
149
+ await req.respond({
150
+ status: 200,
151
+ contentType: mimetype === 'model/gltf+json' ? 'model/gltf+json' : 'application/octet-stream',
152
+ body: contents
153
+ });
154
+ }
155
+ else {
156
+ req.continue();
157
+ }
158
+ });
159
+ await page.goto(VIEWER_URL, { waitUntil: 'domcontentloaded' });
160
+ await page.waitForFunction('window.__gltfReady === true || window.__gltfError', {
161
+ timeout: TIMEOUT_MS
162
+ });
163
+ const err = await page.evaluate('window.__gltfError');
164
+ if (err) {
165
+ env_1.logger.warn(`[attachment] gltf thumbnail error: ${err}`);
166
+ return;
167
+ }
168
+ // puppeteer 신버전은 Uint8Array 를 반환하는데 Uint8Array.toString('base64') 는 encoding 인자를
169
+ // 무시해서 decimal comma string 이 나온다. encoding: 'base64' 를 직접 요청해서 문자열을 받는다.
170
+ const b64 = (await page.screenshot({ type: 'jpeg', quality: 70, encoding: 'base64' }));
171
+ return `data:image/jpeg;base64,${b64}`;
172
+ }
173
+ catch (e) {
174
+ env_1.logger.warn(`[attachment] gltf thumbnail failed: ${e.message}`);
175
+ return;
176
+ }
177
+ finally {
178
+ try {
179
+ await page.close();
180
+ }
181
+ catch { }
182
+ try {
183
+ await pool.release(browser);
184
+ }
185
+ catch { }
186
+ }
187
+ }
188
+ //# sourceMappingURL=gltf-thumbnail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gltf-thumbnail.js","sourceRoot":"","sources":["../../../server/service/attachment/gltf-thumbnail.ts"],"names":[],"mappings":"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","sourcesContent":["import { logger } from '@things-factory/env'\nimport { getOrCreateHeadlessPool } from '@things-factory/shell'\n\n/** 첨부파일 썸네일 전용 headless pool — 보드 pool 과 격리 */\nconst POOL_NAME = 'attachment-thumbnail'\n\nconst W = 400\nconst H = 400\nconst TIMEOUT_MS = 15000\n\n/**\n * 약속: 아래 virtual URL 들은 request interception 으로만 의미를 가짐.\n * - /__viewer__ : 뷰어 HTML\n * - /__gltf__ : GLTF 바이트 (binary) 또는 JSON (text)\n * puppeteer page 가 내부 네비게이션/fetch 시 이 URL 들을 만나면 mock response.\n */\nconst VIEWER_URL = 'http://attachment-thumbnail.local/__viewer__'\nconst GLTF_URL = 'http://attachment-thumbnail.local/__gltf__'\n\nconst VIEWER_HTML = `<!doctype html>\n<html><head>\n<meta charset=\"utf-8\">\n<style>\n html, body { margin: 0; padding: 0; background: #f5f5f5; }\n canvas { display: block; }\n</style>\n<script type=\"importmap\">\n{\n \"imports\": {\n \"three\": \"https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.module.js\",\n \"three/addons/\": \"https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/\"\n }\n}\n</script>\n</head>\n<body>\n<script type=\"module\">\nimport * as THREE from 'three'\nimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'\nimport { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'\nimport { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'\nimport { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'\n\nconst W = ${W}, H = ${H}\nwindow.__gltfReady = false\nwindow.__gltfError = null\n\nconst renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })\nrenderer.setSize(W, H)\nrenderer.setClearColor(0xf5f5f5, 1)\nrenderer.outputColorSpace = THREE.SRGBColorSpace\ndocument.body.appendChild(renderer.domElement)\n\nconst scene = new THREE.Scene()\nconst camera = new THREE.PerspectiveCamera(35, W / H, 0.1, 10000)\n\n// 기본 조명 — 모델 세부 손실 최소화\nscene.add(new THREE.AmbientLight(0xffffff, 0.6))\nconst hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8)\nscene.add(hemi)\nconst dir = new THREE.DirectionalLight(0xffffff, 1.2)\ndir.position.set(5, 10, 7.5)\nscene.add(dir)\n\n;(async () => {\n try {\n const res = await fetch('${GLTF_URL}')\n if (!res.ok) throw new Error('fetch failed: ' + res.status)\n const ab = await res.arrayBuffer()\n\n const loader = new GLTFLoader()\n\n // DRACO 압축 메쉬 지원 (필요한 GLB 만 로드 시 decoder fetch)\n const draco = new DRACOLoader()\n draco.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/draco/')\n loader.setDRACOLoader(draco)\n\n // KTX2 (BasisU) 압축 텍스처 지원\n const ktx2 = new KTX2Loader()\n ktx2.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/libs/basis/')\n ktx2.detectSupport(renderer)\n loader.setKTX2Loader(ktx2)\n\n // Meshopt 압축 지원\n loader.setMeshoptDecoder(MeshoptDecoder)\n\n const gltf = await loader.parseAsync(ab, '')\n scene.add(gltf.scene)\n\n // Bounding box 기반 카메라 auto-fit\n const box = new THREE.Box3().setFromObject(gltf.scene)\n const size = box.getSize(new THREE.Vector3())\n const center = box.getCenter(new THREE.Vector3())\n const maxDim = Math.max(size.x, size.y, size.z) || 1\n const fov = camera.fov * (Math.PI / 180)\n const distance = (maxDim / 2) / Math.tan(fov / 2) * 2.0\n\n // 45도 정면-위쪽 뷰포인트 — 3D 모델 특징이 잘 드러나는 각도\n const dirVec = new THREE.Vector3(1, 0.7, 1).normalize()\n camera.position.copy(center).add(dirVec.multiplyScalar(distance))\n camera.near = Math.max(distance / 100, 0.01)\n camera.far = distance * 10\n camera.updateProjectionMatrix()\n camera.lookAt(center)\n\n renderer.render(scene, camera)\n\n // 렌더 완료를 다음 rAF 까지 보장\n requestAnimationFrame(() => {\n renderer.render(scene, camera)\n window.__gltfReady = true\n })\n } catch (e) {\n window.__gltfError = e?.message || String(e)\n }\n})()\n</script>\n</body></html>`\n\n/**\n * GLTF/GLB 바이트로부터 정적 JPEG 썸네일 data URI 생성.\n * headless Chrome 에 Three.js 로 렌더링된 단일 프레임을 캡처한다.\n * 지원 타입: model/gltf-binary (.glb), model/gltf+json (.gltf)\n */\nexport async function generateGltfThumbnail(\n contents: Buffer | undefined | null,\n mimetype: string | undefined\n): Promise<string | undefined> {\n if (!contents) return\n if (mimetype !== 'model/gltf-binary' && mimetype !== 'model/gltf+json') return\n\n let pool: any\n try {\n pool = getOrCreateHeadlessPool(POOL_NAME, { min: 0, max: 2 })\n } catch (e) {\n logger.warn(`[attachment] gltf thumbnail — headless pool unavailable: ${(e as Error).message}`)\n return\n }\n\n const browser = await pool.acquire()\n if (!browser) return\n\n const page = await browser.newPage()\n try {\n await page.setViewport({ width: W, height: H, deviceScaleFactor: 1 })\n await page.setRequestInterception(true)\n\n page.on('request', async (req: any) => {\n const url = req.url()\n if (url === VIEWER_URL) {\n await req.respond({ status: 200, contentType: 'text/html; charset=utf-8', body: VIEWER_HTML })\n } else if (url === GLTF_URL) {\n await req.respond({\n status: 200,\n contentType: mimetype === 'model/gltf+json' ? 'model/gltf+json' : 'application/octet-stream',\n body: contents\n })\n } else {\n req.continue()\n }\n })\n\n await page.goto(VIEWER_URL, { waitUntil: 'domcontentloaded' })\n\n await page.waitForFunction('window.__gltfReady === true || window.__gltfError', {\n timeout: TIMEOUT_MS\n })\n\n const err = await page.evaluate('window.__gltfError')\n if (err) {\n logger.warn(`[attachment] gltf thumbnail error: ${err}`)\n return\n }\n\n // puppeteer 신버전은 Uint8Array 를 반환하는데 Uint8Array.toString('base64') 는 encoding 인자를\n // 무시해서 decimal comma string 이 나온다. encoding: 'base64' 를 직접 요청해서 문자열을 받는다.\n const b64 = (await page.screenshot({ type: 'jpeg', quality: 70, encoding: 'base64' })) as string\n return `data:image/jpeg;base64,${b64}`\n } catch (e) {\n logger.warn(`[attachment] gltf thumbnail failed: ${(e as Error).message}`)\n return\n } finally {\n try { await page.close() } catch {}\n try { await pool.release(browser) } catch {}\n }\n}\n"]}
@@ -0,0 +1,17 @@
1
+ export declare class ThumbnailBackfillResult {
2
+ attempted: number;
3
+ succeeded: number;
4
+ failed: number;
5
+ remaining: number;
6
+ }
7
+ /**
8
+ * 썸네일이 없는 첨부파일을 조회해서 서버에서 썸네일을 생성·저장한다.
9
+ * 하나의 호출당 최대 `limit` 개까지만 처리 — 장시간 점유 방지.
10
+ * 결과의 `remaining > 0` 이면 동일 호출을 반복해 배치 완료시킴.
11
+ *
12
+ * 대상 mimetype 만 시도 (현재 image/*, model/gltf-*).
13
+ * contents 컬럼이 비어 있고 STORAGE.readFile 로도 못 읽으면 skip.
14
+ */
15
+ export declare function backfillAttachmentThumbnails({ limit }: {
16
+ limit?: number;
17
+ }, context: ResolverContext): Promise<ThumbnailBackfillResult>;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ThumbnailBackfillResult = void 0;
4
+ exports.backfillAttachmentThumbnails = backfillAttachmentThumbnails;
5
+ const tslib_1 = require("tslib");
6
+ const type_graphql_1 = require("type-graphql");
7
+ const env_1 = require("@things-factory/env");
8
+ const shell_1 = require("@things-factory/shell");
9
+ const attachment_const_1 = require("../../attachment-const");
10
+ const attachment_1 = require("./attachment");
11
+ const thumbnail_1 = require("./thumbnail");
12
+ let ThumbnailBackfillResult = class ThumbnailBackfillResult {
13
+ };
14
+ exports.ThumbnailBackfillResult = ThumbnailBackfillResult;
15
+ tslib_1.__decorate([
16
+ (0, type_graphql_1.Field)(() => type_graphql_1.Int, { description: '이번 호출에서 처리 시도한 첨부 개수' }),
17
+ tslib_1.__metadata("design:type", Number)
18
+ ], ThumbnailBackfillResult.prototype, "attempted", void 0);
19
+ tslib_1.__decorate([
20
+ (0, type_graphql_1.Field)(() => type_graphql_1.Int, { description: '썸네일 생성·저장 성공 개수' }),
21
+ tslib_1.__metadata("design:type", Number)
22
+ ], ThumbnailBackfillResult.prototype, "succeeded", void 0);
23
+ tslib_1.__decorate([
24
+ (0, type_graphql_1.Field)(() => type_graphql_1.Int, { description: '실패(생성 실패/콘텐츠 없음 등) 개수' }),
25
+ tslib_1.__metadata("design:type", Number)
26
+ ], ThumbnailBackfillResult.prototype, "failed", void 0);
27
+ tslib_1.__decorate([
28
+ (0, type_graphql_1.Field)(() => type_graphql_1.Int, {
29
+ description: '이번 처리 후에도 남아있는 썸네일 미생성 후보 개수 (대략치). 0 이면 완료'
30
+ }),
31
+ tslib_1.__metadata("design:type", Number)
32
+ ], ThumbnailBackfillResult.prototype, "remaining", void 0);
33
+ exports.ThumbnailBackfillResult = ThumbnailBackfillResult = tslib_1.__decorate([
34
+ (0, type_graphql_1.ObjectType)({ description: '썸네일 백필 결과' })
35
+ ], ThumbnailBackfillResult);
36
+ /**
37
+ * 썸네일이 없는 첨부파일을 조회해서 서버에서 썸네일을 생성·저장한다.
38
+ * 하나의 호출당 최대 `limit` 개까지만 처리 — 장시간 점유 방지.
39
+ * 결과의 `remaining > 0` 이면 동일 호출을 반복해 배치 완료시킴.
40
+ *
41
+ * 대상 mimetype 만 시도 (현재 image/*, model/gltf-*).
42
+ * contents 컬럼이 비어 있고 STORAGE.readFile 로도 못 읽으면 skip.
43
+ */
44
+ async function backfillAttachmentThumbnails({ limit = 20 }, context) {
45
+ // ResolverContext 은 things-factory 런타임 전역 타입
46
+ const { domain, tx } = context.state;
47
+ const repository = tx ? tx.getRepository(attachment_1.Attachment) : (0, shell_1.getRepository)(attachment_1.Attachment);
48
+ // 지원 mimetype 중 썸네일 미생성 대상만 조회
49
+ const qb = repository.createQueryBuilder('att')
50
+ .where('att.domain_id = :domainId', { domainId: domain.id })
51
+ .andWhere('att.thumbnail IS NULL')
52
+ .andWhere('(att.mimetype LIKE :imagePrefix OR att.mimetype IN (:...gltfTypes))', {
53
+ imagePrefix: 'image/%',
54
+ gltfTypes: ['model/gltf-binary', 'model/gltf+json']
55
+ });
56
+ const candidates = await qb.take(limit).getMany();
57
+ let succeeded = 0;
58
+ let failed = 0;
59
+ for (const att of candidates) {
60
+ try {
61
+ const contents = await resolveContents(att);
62
+ if (!contents) {
63
+ failed++;
64
+ continue;
65
+ }
66
+ const thumbnail = await (0, thumbnail_1.generateThumbnail)(contents, att.mimetype);
67
+ if (!thumbnail) {
68
+ failed++;
69
+ continue;
70
+ }
71
+ await repository.update({ id: att.id }, { thumbnail });
72
+ succeeded++;
73
+ }
74
+ catch (e) {
75
+ failed++;
76
+ env_1.logger.warn(`[thumbnail-backfill] ${att.id} failed: ${e.message}`);
77
+ }
78
+ }
79
+ // 남은 후보 개수 — 다음 호출 필요 여부 판단용
80
+ const remaining = await qb.getCount();
81
+ return {
82
+ attempted: candidates.length,
83
+ succeeded,
84
+ failed,
85
+ remaining
86
+ };
87
+ }
88
+ /**
89
+ * contents 획득: DB 저장 모드는 att.contents Buffer 직접, 파일/S3/Azure 모드는
90
+ * STORAGE.readFile 로 경로에서 읽는다. 읽기 실패 시 undefined.
91
+ */
92
+ async function resolveContents(att) {
93
+ // @ts-ignore — TypeORM 이 contents 를 Buffer 로 반환 (타입 시그니처는 any)
94
+ const fromDb = att.contents;
95
+ if (fromDb && fromDb.length > 0)
96
+ return fromDb;
97
+ if (!att.path)
98
+ return;
99
+ try {
100
+ const readFile = attachment_const_1.STORAGE.readFile;
101
+ if (typeof readFile !== 'function')
102
+ return;
103
+ const result = readFile(att.path);
104
+ if (typeof result === 'string')
105
+ return Buffer.from(result);
106
+ if (Buffer.isBuffer(result))
107
+ return result;
108
+ return;
109
+ }
110
+ catch (e) {
111
+ env_1.logger.warn(`[thumbnail-backfill] readFile failed for ${att.path}: ${e.message}`);
112
+ return;
113
+ }
114
+ }
115
+ //# sourceMappingURL=thumbnail-backfill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnail-backfill.js","sourceRoot":"","sources":["../../../server/service/attachment/thumbnail-backfill.ts"],"names":[],"mappings":"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","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\nimport { IsNull } from 'typeorm'\n\nimport { logger } from '@things-factory/env'\nimport { getRepository } from '@things-factory/shell'\n\nimport { STORAGE } from '../../attachment-const'\nimport { Attachment } from './attachment'\nimport { generateThumbnail } from './thumbnail'\n\n@ObjectType({ description: '썸네일 백필 결과' })\nexport class ThumbnailBackfillResult {\n @Field(() => Int, { description: '이번 호출에서 처리 시도한 첨부 개수' })\n attempted!: number\n\n @Field(() => Int, { description: '썸네일 생성·저장 성공 개수' })\n succeeded!: number\n\n @Field(() => Int, { description: '실패(생성 실패/콘텐츠 없음 등) 개수' })\n failed!: number\n\n @Field(() => Int, {\n description: '이번 처리 후에도 남아있는 썸네일 미생성 후보 개수 (대략치). 0 이면 완료'\n })\n remaining!: number\n}\n\n/**\n * 썸네일이 없는 첨부파일을 조회해서 서버에서 썸네일을 생성·저장한다.\n * 하나의 호출당 최대 `limit` 개까지만 처리 — 장시간 점유 방지.\n * 결과의 `remaining > 0` 이면 동일 호출을 반복해 배치 완료시킴.\n *\n * 대상 mimetype 만 시도 (현재 image/*, model/gltf-*).\n * contents 컬럼이 비어 있고 STORAGE.readFile 로도 못 읽으면 skip.\n */\nexport async function backfillAttachmentThumbnails(\n { limit = 20 }: { limit?: number },\n context: ResolverContext\n): Promise<ThumbnailBackfillResult> {\n // ResolverContext 은 things-factory 런타임 전역 타입\n const { domain, tx } = context.state\n const repository = tx ? tx.getRepository(Attachment) : getRepository(Attachment)\n\n // 지원 mimetype 중 썸네일 미생성 대상만 조회\n const qb = repository.createQueryBuilder('att')\n .where('att.domain_id = :domainId', { domainId: domain.id })\n .andWhere('att.thumbnail IS NULL')\n .andWhere('(att.mimetype LIKE :imagePrefix OR att.mimetype IN (:...gltfTypes))', {\n imagePrefix: 'image/%',\n gltfTypes: ['model/gltf-binary', 'model/gltf+json']\n })\n\n const candidates = await qb.take(limit).getMany()\n\n let succeeded = 0\n let failed = 0\n\n for (const att of candidates) {\n try {\n const contents = await resolveContents(att)\n if (!contents) {\n failed++\n continue\n }\n\n const thumbnail = await generateThumbnail(contents, att.mimetype)\n if (!thumbnail) {\n failed++\n continue\n }\n\n await repository.update({ id: att.id }, { thumbnail })\n succeeded++\n } catch (e) {\n failed++\n logger.warn(`[thumbnail-backfill] ${att.id} failed: ${(e as Error).message}`)\n }\n }\n\n // 남은 후보 개수 — 다음 호출 필요 여부 판단용\n const remaining = await qb.getCount()\n\n return {\n attempted: candidates.length,\n succeeded,\n failed,\n remaining\n }\n}\n\n/**\n * contents 획득: DB 저장 모드는 att.contents Buffer 직접, 파일/S3/Azure 모드는\n * STORAGE.readFile 로 경로에서 읽는다. 읽기 실패 시 undefined.\n */\nasync function resolveContents(att: Attachment): Promise<Buffer | undefined> {\n // @ts-ignore — TypeORM 이 contents 를 Buffer 로 반환 (타입 시그니처는 any)\n const fromDb: Buffer | null | undefined = (att as any).contents\n if (fromDb && fromDb.length > 0) return fromDb\n\n if (!att.path) return\n\n try {\n const readFile = (STORAGE as any).readFile as\n | ((path: string, encoding?: string) => Buffer | string)\n | undefined\n if (typeof readFile !== 'function') return\n const result = readFile(att.path)\n if (typeof result === 'string') return Buffer.from(result)\n if (Buffer.isBuffer(result)) return result\n return\n } catch (e) {\n logger.warn(`[thumbnail-backfill] readFile failed for ${att.path}: ${(e as Error).message}`)\n return\n }\n}\n"]}