@elek-io/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2250 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/index.ts
8
+ import {
9
+ constructorElekIoCoreSchema,
10
+ environmentSchema
11
+ } from "@elek-io/shared";
12
+ import Fs8 from "fs-extra";
13
+
14
+ // src/service/AssetService.ts
15
+ import {
16
+ assetFileSchema,
17
+ assetSchema,
18
+ countAssetsSchema,
19
+ createAssetSchema,
20
+ currentTimestamp,
21
+ deleteAssetSchema,
22
+ fileTypeSchema as fileTypeSchema2,
23
+ listAssetsSchema,
24
+ readAssetSchema,
25
+ serviceTypeSchema as serviceTypeSchema3,
26
+ supportedExtensionSchema,
27
+ supportedFileTypeSchema,
28
+ supportedMimeTypeSchema,
29
+ updateAssetSchema,
30
+ uuid as uuid2
31
+ } from "@elek-io/shared";
32
+ import Fs3 from "fs-extra";
33
+ import IsSvg from "is-svg";
34
+
35
+ // src/error/RequiredParameterMissingError.ts
36
+ var RequiredParameterMissingError = class extends Error {
37
+ constructor(parameter) {
38
+ super(`Missing required parameter "${parameter}"`);
39
+ this.name = "RequiredParameterMissingError";
40
+ }
41
+ };
42
+
43
+ // src/util/index.ts
44
+ var util_exports = {};
45
+ __export(util_exports, {
46
+ assignDefaultIfMissing: () => assignDefaultIfMissing,
47
+ files: () => files,
48
+ folders: () => folders,
49
+ fromPath: () => fromPath,
50
+ getDuplicates: () => getDuplicates,
51
+ getRelativePath: () => getRelativePath,
52
+ isNoError: () => isNoError,
53
+ notEmpty: () => notEmpty,
54
+ pathTo: () => pathTo,
55
+ returnResolved: () => returnResolved,
56
+ spawnChildProcess: () => spawnChildProcess,
57
+ workingDirectory: () => workingDirectory
58
+ });
59
+ import { projectFolderSchema, uuidSchema } from "@elek-io/shared";
60
+ import { spawn } from "child_process";
61
+ import Fs from "fs-extra";
62
+ import { filter, flatten, groupBy, uniq } from "lodash-es";
63
+ import Os from "os";
64
+ import Path from "path";
65
+ var workingDirectory = Path.join(Os.homedir(), "elek.io");
66
+ var pathTo = {
67
+ tmp: Path.join(workingDirectory, "tmp"),
68
+ userFile: Path.join(workingDirectory, "user.json"),
69
+ // logs: Path.join(workingDirectory, 'logs'),
70
+ projects: Path.join(workingDirectory, "projects"),
71
+ project: (projectId) => {
72
+ return Path.join(pathTo.projects, projectId);
73
+ },
74
+ projectFile: (projectId) => {
75
+ return Path.join(pathTo.project(projectId), "project.json");
76
+ },
77
+ // projectLogs: (projectId: string): string => {
78
+ // return Path.join(pathTo.project(projectId), projectFolderSchema.Enum.logs);
79
+ // },
80
+ // public: (projectId: string): string => {
81
+ // return Path.join(pathTo.project(projectId), 'public');
82
+ // },
83
+ lfs: (projectId) => {
84
+ return Path.join(pathTo.project(projectId), projectFolderSchema.Enum.lfs);
85
+ },
86
+ collections: (projectId) => {
87
+ return Path.join(
88
+ pathTo.project(projectId),
89
+ projectFolderSchema.Enum.collections
90
+ );
91
+ },
92
+ collection: (projectId, id) => {
93
+ return Path.join(pathTo.collections(projectId), id);
94
+ },
95
+ collectionFile: (projectId, id) => {
96
+ return Path.join(pathTo.collection(projectId, id), "collection.json");
97
+ },
98
+ entries: (projectId, collectionId) => {
99
+ return Path.join(pathTo.collection(projectId, collectionId));
100
+ },
101
+ entryFile: (projectId, collectionId, id, language) => {
102
+ return Path.join(
103
+ pathTo.entries(projectId, collectionId),
104
+ `${id}.${language}.json`
105
+ );
106
+ },
107
+ values: (projectId) => {
108
+ return Path.join(pathTo.project(projectId), "values");
109
+ },
110
+ valueFile: (projectId, id, language) => {
111
+ return Path.join(pathTo.values(projectId), `${id}.${language}.json`);
112
+ },
113
+ assets: (projectId) => {
114
+ return Path.join(
115
+ pathTo.project(projectId),
116
+ projectFolderSchema.Enum.assets
117
+ );
118
+ },
119
+ assetFile: (projectId, id, language) => {
120
+ return Path.join(pathTo.assets(projectId), `${id}.${language}.json`);
121
+ },
122
+ asset: (projectId, id, language, extension) => {
123
+ return Path.join(pathTo.lfs(projectId), `${id}.${language}.${extension}`);
124
+ }
125
+ };
126
+ var fromPath = {
127
+ projectId: (path) => {
128
+ const startsWith = "projects/";
129
+ const endsWith = "/";
130
+ const start = path.indexOf(startsWith) + startsWith.length;
131
+ if (start === -1) {
132
+ return void 0;
133
+ }
134
+ const end = path.indexOf(endsWith, start);
135
+ const result = path.substring(start, end === -1 ? path.length : end);
136
+ if (result && uuidSchema.safeParse(result).success) {
137
+ return result;
138
+ }
139
+ return void 0;
140
+ }
141
+ };
142
+ function assignDefaultIfMissing(value, defaultsTo) {
143
+ return Object.assign(defaultsTo, value);
144
+ }
145
+ function notEmpty(value) {
146
+ if (value === null || value === void 0) {
147
+ return false;
148
+ }
149
+ if (typeof value === "string") {
150
+ if (value.trim() === "") {
151
+ return false;
152
+ }
153
+ }
154
+ return true;
155
+ }
156
+ function isNoError(item) {
157
+ return item instanceof Error !== true;
158
+ }
159
+ async function returnResolved(promises) {
160
+ const toCheck = [];
161
+ for (let index = 0; index < promises.length; index++) {
162
+ const promise = promises[index];
163
+ if (!promise) {
164
+ throw new Error(`No promise found at index "${index}"`);
165
+ }
166
+ toCheck.push(
167
+ promise.then((result) => {
168
+ return result;
169
+ }).catch((error) => {
170
+ return new Error(error);
171
+ })
172
+ );
173
+ }
174
+ const checked = await Promise.all(toCheck);
175
+ return checked.filter(isNoError);
176
+ }
177
+ function spawnChildProcess(command, args, options) {
178
+ return new Promise((resolve, reject) => {
179
+ const childProcess = spawn(command, args, options);
180
+ let log = "";
181
+ childProcess.stdout.on("data", (data) => {
182
+ log += data;
183
+ });
184
+ childProcess.stderr.on("data", (data) => {
185
+ log += data;
186
+ });
187
+ childProcess.on("error", (error) => {
188
+ throw error;
189
+ });
190
+ childProcess.on("exit", (code) => {
191
+ if (code === 0) {
192
+ return resolve(log);
193
+ }
194
+ return reject(log);
195
+ });
196
+ });
197
+ }
198
+ async function folders(path) {
199
+ const dirent = await Fs.readdir(path, { withFileTypes: true });
200
+ return dirent.filter((dirent2) => {
201
+ return dirent2.isDirectory();
202
+ });
203
+ }
204
+ async function files(path, extension) {
205
+ const dirent = await Fs.readdir(path, { withFileTypes: true });
206
+ return dirent.filter((dirent2) => {
207
+ if (extension && dirent2.isFile() === true) {
208
+ if (dirent2.name.endsWith(extension)) {
209
+ return true;
210
+ }
211
+ return false;
212
+ }
213
+ return dirent2.isFile();
214
+ });
215
+ }
216
+ function getRelativePath(path) {
217
+ let relativePath = path.replace(workingDirectory, "");
218
+ if (relativePath.startsWith("/")) {
219
+ relativePath = relativePath.substr(1);
220
+ }
221
+ return relativePath;
222
+ }
223
+ function getDuplicates(arr, key) {
224
+ const grouped = groupBy(arr, (item) => {
225
+ return item[key];
226
+ });
227
+ return uniq(
228
+ flatten(
229
+ filter(grouped, (item) => {
230
+ return item.length > 1;
231
+ })
232
+ )
233
+ );
234
+ }
235
+
236
+ // src/service/AbstractCrudService.ts
237
+ import {
238
+ fileReferenceSchema,
239
+ fileTypeSchema,
240
+ gitCommitIconSchema
241
+ } from "@elek-io/shared";
242
+ import { orderBy, remove } from "lodash-es";
243
+ var AbstractCrudService = class {
244
+ /**
245
+ * Do not instantiate directly as this is an abstract class
246
+ */
247
+ constructor(type, options) {
248
+ this.type = type;
249
+ this.options = options;
250
+ this.gitMessage = {
251
+ create: `${gitCommitIconSchema.enum.CREATE} Created ${this.type}`,
252
+ update: `${gitCommitIconSchema.enum.UPDATE} Updated ${this.type}`,
253
+ delete: `${gitCommitIconSchema.enum.DELETE} Deleted ${this.type}`
254
+ };
255
+ }
256
+ /**
257
+ * Returns the filtered, sorted and paginated version of given list
258
+ *
259
+ * @todo Sorting and filtering requires all models to be loaded
260
+ * from disk. This results in a huge memory spike before the
261
+ * filtering and pagination takes effect - removing most of it again.
262
+ * This approach is still better than returning everything and
263
+ * letting the frontend handle it, since the memory usage would then be constant.
264
+ * But this still could fill the memory limit of node.js (default 1,4 GB).
265
+ *
266
+ * @param list Array to filter, sort and paginate
267
+ * @param sort Array of sort objects containing information about what to sort and how
268
+ * @param filter Filter all object values of `list` by this string
269
+ * @param limit Limit the result to this amount. If 0 is given, no limit is applied
270
+ * @param offset Start at this index instead of 0
271
+ */
272
+ async paginate(list, sort = [], filter2 = "", limit = 15, offset = 0) {
273
+ let result = list;
274
+ const total = list.length;
275
+ const normalizedFilter = filter2.trim().toLowerCase();
276
+ if (normalizedFilter !== "") {
277
+ remove(result, (model) => {
278
+ let key;
279
+ for (key in model) {
280
+ const value = model[key];
281
+ if (String(value).toLowerCase().includes(normalizedFilter)) {
282
+ return false;
283
+ }
284
+ }
285
+ return true;
286
+ });
287
+ }
288
+ if (sort.length !== 0) {
289
+ const keys = sort.map((value) => value.by);
290
+ const orders = sort.map((value) => value.order);
291
+ result = orderBy(result, keys, orders);
292
+ }
293
+ if (limit !== 0) {
294
+ result = result.slice(offset, offset + limit);
295
+ }
296
+ return {
297
+ total,
298
+ limit,
299
+ offset,
300
+ list: result
301
+ };
302
+ }
303
+ /**
304
+ * Returns a list of all file references of given project and type
305
+ *
306
+ * @param type File type of the references wanted
307
+ * @param projectId Project to get all asset references from
308
+ * @param collectionId Only needed when requesting files of type "Entry"
309
+ */
310
+ async listReferences(type, projectId, collectionId) {
311
+ switch (type) {
312
+ case fileTypeSchema.Enum.asset:
313
+ if (!projectId) {
314
+ throw new RequiredParameterMissingError("projectId");
315
+ }
316
+ return this.getFileReferences(pathTo.lfs(projectId));
317
+ case fileTypeSchema.Enum.project:
318
+ return this.getFolderReferences(pathTo.projects);
319
+ case fileTypeSchema.Enum.collection:
320
+ if (!projectId) {
321
+ throw new RequiredParameterMissingError("projectId");
322
+ }
323
+ return this.getFolderReferences(pathTo.collections(projectId));
324
+ case fileTypeSchema.Enum.entry:
325
+ if (!projectId) {
326
+ throw new RequiredParameterMissingError("projectId");
327
+ }
328
+ if (!collectionId) {
329
+ throw new RequiredParameterMissingError("collectionId");
330
+ }
331
+ return this.getFileReferences(
332
+ pathTo.collection(projectId, collectionId)
333
+ );
334
+ case fileTypeSchema.Enum.value:
335
+ if (!projectId) {
336
+ throw new RequiredParameterMissingError("projectId");
337
+ }
338
+ return this.getFileReferences(pathTo.values(projectId));
339
+ default:
340
+ throw new Error(`Trying to list files of unsupported type "${type}"`);
341
+ }
342
+ }
343
+ async getFolderReferences(path) {
344
+ const possibleFolders = await folders(path);
345
+ const results = possibleFolders.map((possibleFolder) => {
346
+ const folderReference = {
347
+ id: possibleFolder.name
348
+ };
349
+ try {
350
+ return fileReferenceSchema.parse(folderReference);
351
+ } catch (error) {
352
+ return null;
353
+ }
354
+ });
355
+ return results.filter(notEmpty);
356
+ }
357
+ /**
358
+ * Searches for all files inside given folder,
359
+ * parses their names and returns them as FileReference
360
+ *
361
+ * Ignores files not matching the [id].[language].[extension]
362
+ * or [id].[extension] format for their names
363
+ */
364
+ async getFileReferences(path) {
365
+ const possibleFiles = await files(path);
366
+ const results = await Promise.all(
367
+ possibleFiles.map(async (possibleFile) => {
368
+ const fileNameArray = possibleFile.name.split(".");
369
+ const fileReference = {
370
+ id: fileNameArray[0],
371
+ language: fileNameArray.length === 3 ? fileNameArray[1] : void 0,
372
+ extension: fileNameArray.length === 2 ? fileNameArray[1] : fileNameArray[2]
373
+ };
374
+ try {
375
+ return fileReferenceSchema.parse(fileReference);
376
+ } catch (error) {
377
+ return null;
378
+ }
379
+ })
380
+ );
381
+ return results.filter(notEmpty);
382
+ }
383
+ };
384
+
385
+ // src/service/GitService.ts
386
+ import {
387
+ gitCommitSchema,
388
+ uuidSchema as uuidSchema2
389
+ } from "@elek-io/shared";
390
+ import { GitProcess } from "dugite";
391
+ import PQueue from "p-queue";
392
+
393
+ // src/error/GitError.ts
394
+ var GitError = class extends Error {
395
+ constructor(message) {
396
+ super(message);
397
+ this.name = "GitError";
398
+ }
399
+ };
400
+
401
+ // src/error/NoCurrentUserError.ts
402
+ var NoCurrentUserError = class extends Error {
403
+ constructor() {
404
+ super("Make sure to set a User via Core before using other methods");
405
+ this.name = "NoCurrentUserError";
406
+ }
407
+ };
408
+
409
+ // src/service/GitTagService.ts
410
+ import {
411
+ countGitTagsSchema,
412
+ createGitTagSchema,
413
+ deleteGitTagSchema,
414
+ gitTagSchema,
415
+ listGitTagsSchema,
416
+ readGitTagSchema,
417
+ serviceTypeSchema,
418
+ uuid
419
+ } from "@elek-io/shared";
420
+ var GitTagService = class extends AbstractCrudService {
421
+ constructor(options, git) {
422
+ super(serviceTypeSchema.Enum.GitTag, options);
423
+ this.git = git;
424
+ }
425
+ /**
426
+ * Creates a new tag
427
+ *
428
+ * @see https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---annotate
429
+ */
430
+ async create(props) {
431
+ createGitTagSchema.parse(props);
432
+ const id = uuid();
433
+ let args = ["tag", "--annotate", id];
434
+ if (props.hash) {
435
+ args = [...args, props.hash];
436
+ }
437
+ args = [...args, "-m", props.message];
438
+ await this.git(props.path, args);
439
+ const tag = await this.read({ path: props.path, id });
440
+ return tag;
441
+ }
442
+ /**
443
+ * Returns a tag by ID
444
+ *
445
+ * Internally uses list() with id as pattern.
446
+ */
447
+ async read(props) {
448
+ readGitTagSchema.parse(props);
449
+ const tags = await this.list({ path: props.path, filter: props.id });
450
+ if (tags.total === 0) {
451
+ throw new GitError(
452
+ `Provided tag with UUID "${props.id}" did not match any known tags`
453
+ );
454
+ }
455
+ if (tags.total > 1) {
456
+ throw new GitError(
457
+ `Provided tag with UUID "${props.id}" matched multiple known tags`
458
+ );
459
+ }
460
+ return tags.list[0];
461
+ }
462
+ /**
463
+ * Updating a git tag is not supported.
464
+ * Please delete the old and create a new one
465
+ *
466
+ * @see https://git-scm.com/docs/git-tag#_on_re_tagging
467
+ */
468
+ async update() {
469
+ throw new Error(
470
+ "Updating a git tag is not supported. Please delete the old and create a new one"
471
+ );
472
+ }
473
+ /**
474
+ * Deletes a tag
475
+ *
476
+ * @see https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---delete
477
+ *
478
+ * @param path Path to the repository
479
+ * @param id UUID of the tag to delete
480
+ */
481
+ async delete(props) {
482
+ deleteGitTagSchema.parse(props);
483
+ const args = ["tag", "--delete", props.id];
484
+ await this.git(props.path, args);
485
+ }
486
+ /**
487
+ * Gets all local tags or filter them by pattern
488
+ *
489
+ * They are sorted by authordate of the commit, not the timestamp the tag is created.
490
+ * This ensures tags are sorted correctly in the timeline of their commits.
491
+ *
492
+ * @see https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---list
493
+ */
494
+ async list(props) {
495
+ listGitTagsSchema.parse(props);
496
+ let args = ["tag", "--list"];
497
+ args = [
498
+ ...args,
499
+ "--sort=-*authordate",
500
+ "--format=%(refname:short)|%(subject)|%(*authorname)|%(*authoremail)|%(*authordate:unix)"
501
+ ];
502
+ const result = await this.git(props.path, args);
503
+ const noEmptyLinesArr = result.stdout.split("\n").filter((line) => {
504
+ return line !== "";
505
+ });
506
+ const lineObjArr = noEmptyLinesArr.map((line) => {
507
+ const lineArray = line.split("|");
508
+ return {
509
+ id: lineArray[0],
510
+ message: lineArray[1],
511
+ author: {
512
+ name: lineArray[2],
513
+ email: lineArray[3]
514
+ },
515
+ timestamp: typeof lineArray[4] === "string" ? parseInt(lineArray[4]) : void 0
516
+ };
517
+ });
518
+ const gitTags = lineObjArr.filter(this.isGitTag.bind(this));
519
+ return this.paginate(
520
+ gitTags,
521
+ props?.sort,
522
+ props?.filter,
523
+ props?.limit,
524
+ props?.offset
525
+ );
526
+ }
527
+ /**
528
+ * Returns the total number of tags inside given repository
529
+ *
530
+ * Internally uses list(), so do not use count()
531
+ * in conjuncion with it to avoid multiple git calls.
532
+ *
533
+ * @param path Path to the repository
534
+ */
535
+ async count(props) {
536
+ countGitTagsSchema.parse(props);
537
+ const gitTags = await this.list({ path: props.path });
538
+ return gitTags.total;
539
+ }
540
+ /**
541
+ * Type guard for GitTag
542
+ *
543
+ * @param obj The object to check
544
+ */
545
+ isGitTag(obj) {
546
+ return gitTagSchema.safeParse(obj).success;
547
+ }
548
+ };
549
+
550
+ // src/service/UserService.ts
551
+ import {
552
+ UserTypeSchema,
553
+ setUserSchema,
554
+ userFileSchema
555
+ } from "@elek-io/shared";
556
+
557
+ // src/service/JsonFileService.ts
558
+ import {
559
+ serviceTypeSchema as serviceTypeSchema2
560
+ } from "@elek-io/shared";
561
+ import Fs2 from "fs-extra";
562
+ var JsonFileService = class extends AbstractCrudService {
563
+ constructor(options) {
564
+ super(serviceTypeSchema2.Enum.JsonFile, options);
565
+ this.cache = /* @__PURE__ */ new Map();
566
+ }
567
+ /**
568
+ * Creates a new file on disk. Fails if path already exists
569
+ *
570
+ * @param data Data to write into the file
571
+ * @param path Path to write the file to
572
+ * @param schema Schema of the file to validate against
573
+ * @returns Validated content of the file from disk
574
+ */
575
+ async create(data, path, schema) {
576
+ const parsedData = schema.parse(data);
577
+ const string = this.serialize(parsedData);
578
+ await Fs2.writeFile(path, string, {
579
+ flag: "wx",
580
+ encoding: "utf8"
581
+ });
582
+ this.cache.set(path, parsedData);
583
+ return parsedData;
584
+ }
585
+ /**
586
+ * Reads the content of a file on disk. Fails if path does not exist
587
+ *
588
+ * @param path Path to read the file from
589
+ * @param schema Schema of the file to validate against
590
+ * @returns Validated content of the file from disk
591
+ */
592
+ async read(path, schema) {
593
+ if (this.cache.has(path)) {
594
+ return this.cache.get(path);
595
+ }
596
+ const data = await Fs2.readFile(path, {
597
+ flag: "r",
598
+ encoding: "utf8"
599
+ });
600
+ const json = this.deserialize(data);
601
+ const parsedData = schema.parse(json);
602
+ this.cache.set(path, parsedData);
603
+ return parsedData;
604
+ }
605
+ /**
606
+ * Overwrites an existing file on disk
607
+ *
608
+ * @todo Check how to error out if the file does not exist already
609
+ *
610
+ * @param data Data to write into the file
611
+ * @param path Path to the file to overwrite
612
+ * @param schema Schema of the file to validate against
613
+ * @returns Validated content of the file from disk
614
+ */
615
+ async update(data, path, schema) {
616
+ const parsedData = schema.parse(data);
617
+ const string = this.serialize(parsedData);
618
+ await Fs2.writeFile(path, string, {
619
+ flag: "w",
620
+ encoding: "utf8"
621
+ });
622
+ this.cache.set(path, parsedData);
623
+ return parsedData;
624
+ }
625
+ serialize(data) {
626
+ return JSON.stringify(data, null, this.options.file.json.indentation);
627
+ }
628
+ deserialize(data) {
629
+ return JSON.parse(data);
630
+ }
631
+ };
632
+
633
+ // src/service/UserService.ts
634
+ var UserService = class {
635
+ constructor(jsonFileService) {
636
+ this.jsonFileService = jsonFileService;
637
+ }
638
+ /**
639
+ * Returns the User currently working with Core
640
+ */
641
+ async get() {
642
+ try {
643
+ return await this.jsonFileService.read(
644
+ pathTo.userFile,
645
+ userFileSchema
646
+ );
647
+ } catch (error) {
648
+ return void 0;
649
+ }
650
+ }
651
+ /**
652
+ * Sets the User currently working with Core
653
+ *
654
+ * By doing so all git operations are done with the signature of this User
655
+ */
656
+ async set(props) {
657
+ setUserSchema.parse(props);
658
+ const userFilePath = pathTo.userFile;
659
+ const userFile = {
660
+ ...props
661
+ };
662
+ if (userFile.userType === UserTypeSchema.Enum.cloud) {
663
+ }
664
+ await this.jsonFileService.update(userFile, userFilePath, userFileSchema);
665
+ return userFile;
666
+ }
667
+ };
668
+
669
+ // src/service/GitService.ts
670
+ var GitService2 = class {
671
+ constructor(options, userService) {
672
+ this.version = void 0;
673
+ this.queue = new PQueue({
674
+ concurrency: 1
675
+ // No concurrency because git operations are sequencial
676
+ });
677
+ this.gitTagService = new GitTagService(options, this.git);
678
+ this.userService = userService;
679
+ }
680
+ /**
681
+ * CRUD methods to work with git tags
682
+ */
683
+ get tags() {
684
+ return this.gitTagService;
685
+ }
686
+ /**
687
+ * Reads the currently used version of Git
688
+ */
689
+ async getVersion() {
690
+ const result = await this.git("", ["--version"]);
691
+ this.version = result.stdout.replace("git version", "").trim();
692
+ }
693
+ /**
694
+ * Create an empty Git repository or reinitialize an existing one
695
+ *
696
+ * @see https://git-scm.com/docs/git-init
697
+ *
698
+ * @param path Path to initialize in. Fails if path does not exist
699
+ * @param options Options specific to the init operation
700
+ */
701
+ async init(path, options) {
702
+ let args = ["init"];
703
+ if (options?.initialBranch) {
704
+ args = [...args, `--initial-branch=${options.initialBranch}`];
705
+ }
706
+ await this.git(path, args);
707
+ await this.setLocalConfig(path);
708
+ await this.installLfs(path);
709
+ }
710
+ /**
711
+ * Clone a repository into a directory
712
+ *
713
+ * @see https://git-scm.com/docs/git-clone
714
+ *
715
+ * @todo Implement progress callback / events
716
+ *
717
+ * @param url The remote repository URL to clone from
718
+ * @param path The destination path for the cloned repository.
719
+ * Which is only working if the directory is existing and empty.
720
+ * @param options Options specific to the clone operation
721
+ */
722
+ async clone(url, path, options) {
723
+ let args = ["clone", "--progress"];
724
+ if (options?.branch) {
725
+ args = [...args, "--branch", options.branch];
726
+ }
727
+ if (options?.depth) {
728
+ args = [...args, "--depth", options.depth.toString()];
729
+ }
730
+ if (options?.singleBranch === true) {
731
+ args = [...args, "--single-branch"];
732
+ }
733
+ await this.git(path, [...args, url, "."]);
734
+ await this.setLocalConfig(path);
735
+ }
736
+ /**
737
+ * Add file contents to the index
738
+ *
739
+ * @see https://git-scm.com/docs/git-add
740
+ *
741
+ * @param path Path to the repository
742
+ * @param files Files to add
743
+ */
744
+ async add(path, files2) {
745
+ const args = ["add", "--", ...files2];
746
+ await this.git(path, args);
747
+ }
748
+ /**
749
+ * Switch branches
750
+ *
751
+ * @see https://git-scm.com/docs/git-switch/
752
+ *
753
+ * @param path Path to the repository
754
+ * @param name Name of the branch to switch to
755
+ * @param options Options specific to the switch operation
756
+ */
757
+ async switch(path, name, options) {
758
+ await this.checkBranchOrTagName(path, name);
759
+ let args = ["switch"];
760
+ if (options?.isNew === true) {
761
+ args = [...args, "--create", name];
762
+ } else {
763
+ args = [...args, name];
764
+ }
765
+ await this.git(path, args);
766
+ }
767
+ /**
768
+ * Reset current HEAD to the specified state
769
+ *
770
+ * @todo maybe add more options
771
+ * @see https://git-scm.com/docs/git-reset
772
+ *
773
+ * @param path Path to the repository
774
+ * @param mode Modifies the working tree depending on given mode
775
+ * @param commit Resets the current branch head to this commit / tag
776
+ */
777
+ async reset(path, mode, commit) {
778
+ const args = ["reset", `--${mode}`, commit];
779
+ await this.git(path, args);
780
+ }
781
+ /**
782
+ * Restore working tree files
783
+ *
784
+ * @see https://git-scm.com/docs/git-restore/
785
+ *
786
+ * @todo It's probably a good idea to not use restore
787
+ * for a use case where someone just wants to have a look
788
+ * and maybe copy something from a deleted file.
789
+ * We should use `checkout` without `add .` and `commit` for that
790
+ *
791
+ * @param path Path to the repository
792
+ * @param source Git commit SHA or tag name to restore to
793
+ * @param files Files to restore
794
+ */
795
+ // public async restore(
796
+ // path: string,
797
+ // source: string,
798
+ // files: string[]
799
+ // ): Promise<void> {
800
+ // const args = ['restore', `--source=${source}`, ...files];
801
+ // await this.git(path, args);
802
+ // }
803
+ /**
804
+ * Fetch from and integrate with another repository or a local branch
805
+ *
806
+ * @see https://git-scm.com/docs/git-pull
807
+ *
808
+ * @param path Path to the repository
809
+ */
810
+ async pull(path) {
811
+ const args = ["pull"];
812
+ await this.git(path, args);
813
+ }
814
+ /**
815
+ * Record changes to the repository
816
+ *
817
+ * @see https://git-scm.com/docs/git-commit
818
+ *
819
+ * @param path Path to the repository
820
+ * @param message A message that describes the changes
821
+ */
822
+ async commit(path, message) {
823
+ const user = await this.userService.get();
824
+ if (!user) {
825
+ throw new NoCurrentUserError();
826
+ }
827
+ const args = [
828
+ "commit",
829
+ `--message=${message}`,
830
+ `--author=${user.name} <${user.email}>`
831
+ ];
832
+ await this.git(path, args);
833
+ }
834
+ /**
835
+ * Gets local commit history
836
+ *
837
+ * @see https://git-scm.com/docs/git-log
838
+ *
839
+ * @todo Check if there is a need to trim the git commit message of chars
840
+ * @todo Use this method in a service. Decide if we need a HistoryService for example
841
+ *
842
+ * @param path Path to the repository
843
+ * @param options Options specific to the log operation
844
+ */
845
+ async log(path, options) {
846
+ let args = ["log"];
847
+ if (options?.between?.from) {
848
+ args = [
849
+ ...args,
850
+ `${options.between.from}...${options.between.to || "HEAD"}`
851
+ ];
852
+ }
853
+ if (options?.limit) {
854
+ args = [...args, `--max-count=${options.limit}`];
855
+ }
856
+ const result = await this.git(path, [
857
+ ...args,
858
+ "--format=%H|%s|%an|%ae|%at|%D"
859
+ ]);
860
+ const noEmptyLinesArr = result.stdout.split("\n").filter((line) => {
861
+ return line !== "";
862
+ });
863
+ const lineObjArr = noEmptyLinesArr.map((line) => {
864
+ const lineArray = line.split("|");
865
+ return {
866
+ hash: lineArray[0],
867
+ message: lineArray[1],
868
+ author: {
869
+ name: lineArray[2],
870
+ email: lineArray[3]
871
+ },
872
+ timestamp: parseInt(lineArray[4]),
873
+ tag: this.refNameToTagName(lineArray[5])
874
+ };
875
+ });
876
+ return lineObjArr.filter(this.isGitCommit.bind(this));
877
+ }
878
+ refNameToTagName(refName) {
879
+ let tagName = "";
880
+ tagName = refName.replace("tag: ", "");
881
+ if (tagName.trim() === "" || uuidSchema2.safeParse(tagName).success === false) {
882
+ tagName = void 0;
883
+ }
884
+ return tagName;
885
+ }
886
+ /**
887
+ * Returns a timestamp of given files creation
888
+ *
889
+ * Git only returns the timestamp the file was added,
890
+ * which could be different from the file being created.
891
+ * But since file operations will always be committed
892
+ * immediately, this is practically the same.
893
+ *
894
+ * @param path Path to the repository
895
+ * @param file File to get timestamp from
896
+ */
897
+ async getFileCreatedTimestamp(path, file) {
898
+ const result = await this.git(path, [
899
+ "log",
900
+ "--diff-filter=A",
901
+ "--follow",
902
+ "--format=%at",
903
+ "--max-count=1",
904
+ "--",
905
+ file
906
+ ]);
907
+ return parseInt(result.stdout);
908
+ }
909
+ /**
910
+ * Returns a timestamp of the files last modification
911
+ *
912
+ * @param path Path to the repository
913
+ * @param file File to get timestamp from
914
+ */
915
+ async getFileLastUpdatedTimestamp(path, file) {
916
+ const result = await this.git(path, [
917
+ "log",
918
+ "--follow",
919
+ "--format=%at",
920
+ "--max-count=1",
921
+ "--",
922
+ file
923
+ ]);
924
+ return parseInt(result.stdout);
925
+ }
926
+ /**
927
+ * Returns created and updated timestamps from given file
928
+ *
929
+ * @param path Path to the project
930
+ * @param file Path to the file
931
+ */
932
+ async getFileCreatedUpdatedMeta(path, file) {
933
+ const meta = await Promise.all([
934
+ this.getFileCreatedTimestamp(path, file),
935
+ this.getFileLastUpdatedTimestamp(path, file)
936
+ ]);
937
+ return {
938
+ created: meta[0],
939
+ updated: meta[1]
940
+ };
941
+ }
942
+ /**
943
+ * A reference is used in Git to specify branches and tags.
944
+ * This method checks if given name matches the required format
945
+ *
946
+ * @see https://git-scm.com/docs/git-check-ref-format
947
+ *
948
+ * @param path Path to the repository
949
+ * @param name Name to check
950
+ */
951
+ async checkBranchOrTagName(path, name) {
952
+ await this.git(path, ["check-ref-format", "--allow-onelevel", name]);
953
+ }
954
+ /**
955
+ * Installs LFS support and starts tracking
956
+ * all files inside the lfs folder
957
+ *
958
+ * @param path Path to the repository
959
+ */
960
+ async installLfs(path) {
961
+ await this.git(path, ["lfs", "install"]);
962
+ await this.git(path, ["lfs", "track", "lfs/*"]);
963
+ }
964
+ /**
965
+ * Sets the git config of given local repository from ElekIoCoreOptions
966
+ *
967
+ * @param path Path to the repository
968
+ */
969
+ async setLocalConfig(path) {
970
+ const user = await this.userService.get();
971
+ if (!user) {
972
+ throw new NoCurrentUserError();
973
+ }
974
+ const userNameArgs = ["config", "--local", "user.name", user.name];
975
+ const userEmailArgs = ["config", "--local", "user.email", user.email];
976
+ await this.git(path, userNameArgs);
977
+ await this.git(path, userEmailArgs);
978
+ }
979
+ /**
980
+ * Type guard for GitCommit
981
+ *
982
+ * @param obj The object to check
983
+ */
984
+ isGitCommit(obj) {
985
+ return gitCommitSchema.safeParse(obj).success;
986
+ }
987
+ /**
988
+ * Wraps the execution of any git command
989
+ * to use a FIFO queue for sequential processing
990
+ *
991
+ * @param path Path to the repository
992
+ * @param args Arguments to append after the `git` command
993
+ */
994
+ async git(path, args) {
995
+ const result = await this.queue.add(() => GitProcess.exec(args, path));
996
+ if (!result) {
997
+ throw new GitError(
998
+ `Git (${this.version}) command "git ${args.join(
999
+ " "
1000
+ )}" failed to return a result`
1001
+ );
1002
+ }
1003
+ return result;
1004
+ }
1005
+ };
1006
+
1007
+ // src/service/AssetService.ts
1008
+ var AssetService = class extends AbstractCrudService {
1009
+ constructor(options, jsonFileService, gitService) {
1010
+ super(serviceTypeSchema3.Enum.Asset, options);
1011
+ this.jsonFileService = jsonFileService;
1012
+ this.gitService = gitService;
1013
+ }
1014
+ /**
1015
+ * Creates a new Asset
1016
+ */
1017
+ async create(props) {
1018
+ createAssetSchema.parse(props);
1019
+ const id = uuid2();
1020
+ const projectPath = pathTo.project(props.projectId);
1021
+ const fileType = await this.getSupportedFileTypeOrThrow(props.filePath);
1022
+ const size = await this.getAssetSize(props.filePath);
1023
+ const assetPath = pathTo.asset(
1024
+ props.projectId,
1025
+ id,
1026
+ props.language,
1027
+ fileType.extension
1028
+ );
1029
+ const assetFilePath = pathTo.assetFile(
1030
+ props.projectId,
1031
+ id,
1032
+ props.language
1033
+ );
1034
+ const assetFile = {
1035
+ ...props,
1036
+ fileType: "asset",
1037
+ id,
1038
+ created: currentTimestamp(),
1039
+ extension: fileType.extension,
1040
+ mimeType: fileType.mimeType,
1041
+ size
1042
+ };
1043
+ try {
1044
+ await Fs3.copyFile(props.filePath, assetPath);
1045
+ await this.jsonFileService.create(
1046
+ assetFile,
1047
+ assetFilePath,
1048
+ assetFileSchema
1049
+ );
1050
+ } catch (error) {
1051
+ await this.delete({ ...assetFile, projectId: props.projectId });
1052
+ throw error;
1053
+ }
1054
+ await this.gitService.add(projectPath, [assetFilePath, assetPath]);
1055
+ await this.gitService.commit(projectPath, this.gitMessage.create);
1056
+ return this.toAsset(props.projectId, assetFile);
1057
+ }
1058
+ /**
1059
+ * Returns an Asset by ID and language
1060
+ */
1061
+ async read(props) {
1062
+ readAssetSchema.parse(props);
1063
+ const assetFile = await this.jsonFileService.read(
1064
+ pathTo.assetFile(props.projectId, props.id, props.language),
1065
+ assetFileSchema
1066
+ );
1067
+ return this.toAsset(props.projectId, assetFile);
1068
+ }
1069
+ /**
1070
+ * Updates given Asset
1071
+ *
1072
+ * Use the optional "newFilePath" prop to update the Asset itself
1073
+ */
1074
+ async update(props) {
1075
+ updateAssetSchema.parse(props);
1076
+ const projectPath = pathTo.project(props.projectId);
1077
+ const assetFilePath = pathTo.assetFile(
1078
+ props.projectId,
1079
+ props.id,
1080
+ props.language
1081
+ );
1082
+ const prevAssetFile = await this.read(props);
1083
+ const assetFile = {
1084
+ ...prevAssetFile,
1085
+ ...props,
1086
+ updated: currentTimestamp()
1087
+ };
1088
+ if (props.newFilePath) {
1089
+ const fileType = await this.getSupportedFileTypeOrThrow(
1090
+ props.newFilePath
1091
+ );
1092
+ const size = await this.getAssetSize(props.newFilePath);
1093
+ const prevAssetPath = pathTo.asset(
1094
+ props.projectId,
1095
+ props.id,
1096
+ props.language,
1097
+ prevAssetFile.extension
1098
+ );
1099
+ const assetPath = pathTo.asset(
1100
+ props.projectId,
1101
+ props.id,
1102
+ props.language,
1103
+ fileType.extension
1104
+ );
1105
+ await Fs3.remove(prevAssetPath);
1106
+ await Fs3.copyFile(props.newFilePath, assetPath);
1107
+ assetFile.extension = fileType.extension;
1108
+ assetFile.mimeType = fileType.mimeType;
1109
+ assetFile.size = size;
1110
+ }
1111
+ await this.jsonFileService.update(
1112
+ assetFile,
1113
+ assetFilePath,
1114
+ assetFileSchema
1115
+ );
1116
+ await this.gitService.add(projectPath, [assetFilePath]);
1117
+ await this.gitService.commit(projectPath, this.gitMessage.update);
1118
+ return this.toAsset(props.projectId, assetFile);
1119
+ }
1120
+ /**
1121
+ * Deletes given Asset
1122
+ */
1123
+ async delete(props) {
1124
+ deleteAssetSchema.parse(props);
1125
+ const projectPath = pathTo.project(props.projectId);
1126
+ const assetFilePath = pathTo.assetFile(
1127
+ props.projectId,
1128
+ props.id,
1129
+ props.language
1130
+ );
1131
+ const assetPath = pathTo.asset(
1132
+ props.projectId,
1133
+ props.id,
1134
+ props.language,
1135
+ props.extension
1136
+ );
1137
+ await Fs3.remove(assetPath);
1138
+ await Fs3.remove(assetFilePath);
1139
+ await this.gitService.add(projectPath, [assetFilePath, assetPath]);
1140
+ await this.gitService.commit(projectPath, this.gitMessage.delete);
1141
+ }
1142
+ async list(props) {
1143
+ listAssetsSchema.parse(props);
1144
+ const assetReferences = await this.listReferences(
1145
+ fileTypeSchema2.Enum.asset,
1146
+ props.projectId
1147
+ );
1148
+ const list = await returnResolved(
1149
+ assetReferences.map((assetReference) => {
1150
+ if (!assetReference.language) {
1151
+ throw new RequiredParameterMissingError("language");
1152
+ }
1153
+ return this.read({
1154
+ projectId: props.projectId,
1155
+ id: assetReference.id,
1156
+ language: assetReference.language
1157
+ });
1158
+ })
1159
+ );
1160
+ const paginatedResult = this.paginate(
1161
+ list,
1162
+ props.sort,
1163
+ props.filter,
1164
+ props.limit,
1165
+ props.offset
1166
+ );
1167
+ return paginatedResult;
1168
+ }
1169
+ async count(props) {
1170
+ countAssetsSchema.parse(props);
1171
+ const count = (await this.listReferences(fileTypeSchema2.Enum.asset, props.projectId)).length;
1172
+ return count;
1173
+ }
1174
+ /**
1175
+ * Checks if given object is of type Asset
1176
+ */
1177
+ isAsset(obj) {
1178
+ return assetSchema.safeParse(obj).success;
1179
+ }
1180
+ /**
1181
+ * Returns the size of an Asset in bytes
1182
+ *
1183
+ * @param path Path of the Asset to get the size from
1184
+ */
1185
+ async getAssetSize(path) {
1186
+ return (await Fs3.stat(path)).size;
1187
+ }
1188
+ /**
1189
+ * Creates an Asset from given AssetFile
1190
+ *
1191
+ * @param projectId The project's ID
1192
+ * @param assetFile The AssetFile to convert
1193
+ */
1194
+ async toAsset(projectId, assetFile) {
1195
+ const assetPath = pathTo.asset(
1196
+ projectId,
1197
+ assetFile.id,
1198
+ assetFile.language,
1199
+ assetFile.extension
1200
+ );
1201
+ const asset = {
1202
+ ...assetFile,
1203
+ absolutePath: assetPath
1204
+ };
1205
+ return asset;
1206
+ }
1207
+ /**
1208
+ * Returns the found and supported extension as well as mime type,
1209
+ * otherwise throws an error
1210
+ *
1211
+ * @param filePath Path to the file to check
1212
+ */
1213
+ async getSupportedFileTypeOrThrow(filePath) {
1214
+ const fileSize = (await Fs3.stat(filePath)).size;
1215
+ if (fileSize / 1e3 <= 500) {
1216
+ const fileBuffer = await Fs3.readFile(filePath);
1217
+ if (IsSvg(fileBuffer.toString()) === true) {
1218
+ return {
1219
+ extension: supportedExtensionSchema.Enum.svg,
1220
+ mimeType: supportedMimeTypeSchema.Enum["image/svg+xml"]
1221
+ };
1222
+ }
1223
+ }
1224
+ const { fileTypeFromFile } = await import("file-type");
1225
+ const fileType = await fileTypeFromFile(filePath);
1226
+ const result = supportedFileTypeSchema.parse({
1227
+ extension: fileType?.ext,
1228
+ mimeType: fileType?.mime
1229
+ });
1230
+ return result;
1231
+ }
1232
+ };
1233
+
1234
+ // src/service/CollectionService.ts
1235
+ import {
1236
+ collectionFileSchema,
1237
+ countCollectionsSchema,
1238
+ createCollectionSchema,
1239
+ currentTimestamp as currentTimestamp2,
1240
+ deleteCollectionSchema,
1241
+ fileTypeSchema as fileTypeSchema3,
1242
+ listCollectionsSchema,
1243
+ readCollectionSchema,
1244
+ serviceTypeSchema as serviceTypeSchema4,
1245
+ slug,
1246
+ updateCollectionSchema,
1247
+ uuid as uuid3
1248
+ } from "@elek-io/shared";
1249
+ import Fs4 from "fs-extra";
1250
+ var CollectionService = class extends AbstractCrudService {
1251
+ constructor(options, jsonFileService, gitService) {
1252
+ super(serviceTypeSchema4.Enum.Collection, options);
1253
+ this.jsonFileService = jsonFileService;
1254
+ this.gitService = gitService;
1255
+ }
1256
+ /**
1257
+ * Creates a new Collection
1258
+ */
1259
+ async create(props) {
1260
+ createCollectionSchema.parse(props);
1261
+ const id = uuid3();
1262
+ const projectPath = pathTo.project(props.projectId);
1263
+ const collectionPath = pathTo.collection(props.projectId, id);
1264
+ const collectionFilePath = pathTo.collectionFile(
1265
+ props.projectId,
1266
+ id
1267
+ );
1268
+ const collectionFile = {
1269
+ ...props,
1270
+ fileType: "collection",
1271
+ id,
1272
+ slug: {
1273
+ singular: slug(props.slug.singular),
1274
+ plural: slug(props.slug.plural)
1275
+ },
1276
+ created: currentTimestamp2()
1277
+ };
1278
+ await Fs4.ensureDir(collectionPath);
1279
+ await this.jsonFileService.create(
1280
+ collectionFile,
1281
+ collectionFilePath,
1282
+ collectionFileSchema
1283
+ );
1284
+ await this.gitService.add(projectPath, [collectionFilePath]);
1285
+ await this.gitService.commit(projectPath, this.gitMessage.create);
1286
+ return collectionFile;
1287
+ }
1288
+ /**
1289
+ * Returns a Collection by ID
1290
+ */
1291
+ async read(props) {
1292
+ readCollectionSchema.parse(props);
1293
+ const collection = await this.jsonFileService.read(
1294
+ pathTo.collectionFile(props.projectId, props.id),
1295
+ collectionFileSchema
1296
+ );
1297
+ return collection;
1298
+ }
1299
+ /**
1300
+ * Updates given Collection
1301
+ *
1302
+ * @todo finish implementing checks for FieldDefinitions and extract methods
1303
+ *
1304
+ * @param projectId Project ID of the collection to update
1305
+ * @param collection Collection to write to disk
1306
+ * @returns An object containing information about the actions needed to be taken,
1307
+ * before given update can be executed or void if the update was executed successfully
1308
+ */
1309
+ async update(props) {
1310
+ updateCollectionSchema.parse(props);
1311
+ const projectPath = pathTo.project(props.projectId);
1312
+ const collectionFilePath = pathTo.collectionFile(
1313
+ props.projectId,
1314
+ props.id
1315
+ );
1316
+ const prevCollectionFile = await this.read(props);
1317
+ const collectionFile = {
1318
+ ...prevCollectionFile,
1319
+ ...props,
1320
+ updated: currentTimestamp2()
1321
+ };
1322
+ await this.jsonFileService.update(
1323
+ collectionFile,
1324
+ collectionFilePath,
1325
+ collectionFileSchema
1326
+ );
1327
+ await this.gitService.add(projectPath, [collectionFilePath]);
1328
+ await this.gitService.commit(projectPath, this.gitMessage.update);
1329
+ return collectionFile;
1330
+ }
1331
+ /**
1332
+ * Deletes given Collection (folder), including it's items
1333
+ *
1334
+ * The Fields that Collection used are not deleted.
1335
+ */
1336
+ async delete(props) {
1337
+ deleteCollectionSchema.parse(props);
1338
+ const projectPath = pathTo.project(props.projectId);
1339
+ const collectionPath = pathTo.collection(
1340
+ props.projectId,
1341
+ props.id
1342
+ );
1343
+ await Fs4.remove(collectionPath);
1344
+ await this.gitService.add(projectPath, [collectionPath]);
1345
+ await this.gitService.commit(projectPath, this.gitMessage.delete);
1346
+ }
1347
+ async list(props) {
1348
+ listCollectionsSchema.parse(props);
1349
+ const references = await this.listReferences(
1350
+ fileTypeSchema3.Enum.collection,
1351
+ props.projectId
1352
+ );
1353
+ const list = await returnResolved(
1354
+ references.map((reference) => {
1355
+ return this.read({
1356
+ projectId: props.projectId,
1357
+ id: reference.id
1358
+ });
1359
+ })
1360
+ );
1361
+ return this.paginate(
1362
+ list,
1363
+ props.sort,
1364
+ props.filter,
1365
+ props.limit,
1366
+ props.offset
1367
+ );
1368
+ }
1369
+ async count(props) {
1370
+ countCollectionsSchema.parse(props);
1371
+ const count = (await this.listReferences(fileTypeSchema3.Enum.collection, props.projectId)).length;
1372
+ return count;
1373
+ }
1374
+ /**
1375
+ * Checks if given object is of type Collection
1376
+ */
1377
+ isCollection(obj) {
1378
+ return collectionFileSchema.safeParse(obj).success;
1379
+ }
1380
+ };
1381
+
1382
+ // src/service/EntryService.ts
1383
+ import {
1384
+ countEntriesSchema,
1385
+ createEntrySchema,
1386
+ currentTimestamp as currentTimestamp4,
1387
+ deleteEntrySchema,
1388
+ entryFileSchema,
1389
+ entrySchema,
1390
+ fileTypeSchema as fileTypeSchema5,
1391
+ getValueSchemaFromDefinition as getValueSchemaFromDefinition2,
1392
+ listEntriesSchema,
1393
+ readEntrySchema,
1394
+ serviceTypeSchema as serviceTypeSchema6,
1395
+ updateEntrySchema,
1396
+ uuid as uuid5
1397
+ } from "@elek-io/shared";
1398
+ import Fs6 from "fs-extra";
1399
+
1400
+ // src/service/ValueService.ts
1401
+ import {
1402
+ countValuesSchema,
1403
+ createValueSchema,
1404
+ currentTimestamp as currentTimestamp3,
1405
+ deleteValueSchema,
1406
+ fileTypeSchema as fileTypeSchema4,
1407
+ getValueSchemaFromDefinition,
1408
+ listValuesSchema,
1409
+ readValueSchema,
1410
+ serviceTypeSchema as serviceTypeSchema5,
1411
+ updateValueSchema,
1412
+ uuid as uuid4,
1413
+ validateValueSchema,
1414
+ valueFileSchema
1415
+ } from "@elek-io/shared";
1416
+ import Fs5 from "fs-extra";
1417
+ var ValueService = class extends AbstractCrudService {
1418
+ constructor(options, jsonFileService, gitService, assetService) {
1419
+ super(serviceTypeSchema5.Enum.Value, options);
1420
+ this.jsonFileService = jsonFileService;
1421
+ this.gitService = gitService;
1422
+ }
1423
+ /**
1424
+ * Creates a new Value
1425
+ */
1426
+ async create(props) {
1427
+ createValueSchema.parse(props);
1428
+ const id = uuid4();
1429
+ const projectPath = pathTo.project(props.projectId);
1430
+ const valueFilePath = pathTo.valueFile(
1431
+ props.projectId,
1432
+ id,
1433
+ props.language
1434
+ );
1435
+ const valueFile = {
1436
+ ...props,
1437
+ fileType: "value",
1438
+ id,
1439
+ created: currentTimestamp3()
1440
+ };
1441
+ await this.jsonFileService.create(
1442
+ valueFile,
1443
+ valueFilePath,
1444
+ valueFileSchema
1445
+ );
1446
+ await this.gitService.add(projectPath, [valueFilePath]);
1447
+ await this.gitService.commit(projectPath, this.gitMessage.create);
1448
+ return valueFile;
1449
+ }
1450
+ /**
1451
+ * Returns a Value by ID and language
1452
+ */
1453
+ async read(props) {
1454
+ readValueSchema.parse(props);
1455
+ const valueFile = await this.jsonFileService.read(
1456
+ pathTo.valueFile(props.projectId, props.id, props.language),
1457
+ valueFileSchema
1458
+ );
1459
+ return valueFile;
1460
+ }
1461
+ /**
1462
+ * Updates given Value
1463
+ */
1464
+ async update(props) {
1465
+ updateValueSchema.parse(props);
1466
+ const projectPath = pathTo.project(props.projectId);
1467
+ const valueFilePath = pathTo.valueFile(
1468
+ props.projectId,
1469
+ props.id,
1470
+ props.language
1471
+ );
1472
+ const prevValueFile = await this.read(props);
1473
+ const valueFile = {
1474
+ ...prevValueFile,
1475
+ ...props,
1476
+ updated: currentTimestamp3()
1477
+ };
1478
+ await this.jsonFileService.update(
1479
+ valueFile,
1480
+ valueFilePath,
1481
+ valueFileSchema
1482
+ );
1483
+ await this.gitService.add(projectPath, [valueFilePath]);
1484
+ await this.gitService.commit(projectPath, this.gitMessage.update);
1485
+ return valueFile;
1486
+ }
1487
+ /**
1488
+ * Deletes given Value
1489
+ */
1490
+ async delete(props) {
1491
+ deleteValueSchema.parse(props);
1492
+ const projectPath = pathTo.project(props.projectId);
1493
+ const valueFilePath = pathTo.valueFile(
1494
+ props.projectId,
1495
+ props.id,
1496
+ props.language
1497
+ );
1498
+ await Fs5.remove(valueFilePath);
1499
+ await this.gitService.add(projectPath, [valueFilePath]);
1500
+ await this.gitService.commit(projectPath, this.gitMessage.delete);
1501
+ }
1502
+ async list(props) {
1503
+ listValuesSchema.parse(props);
1504
+ const references = await this.listReferences(
1505
+ fileTypeSchema4.Enum.value,
1506
+ props.projectId
1507
+ );
1508
+ const list = await returnResolved(
1509
+ references.map((reference) => {
1510
+ if (!reference.language) {
1511
+ throw new RequiredParameterMissingError("language");
1512
+ }
1513
+ return this.read({
1514
+ projectId: props.projectId,
1515
+ id: reference.id,
1516
+ language: reference.language
1517
+ });
1518
+ })
1519
+ );
1520
+ return this.paginate(
1521
+ list,
1522
+ props.sort,
1523
+ props.filter,
1524
+ props.limit,
1525
+ props.offset
1526
+ );
1527
+ }
1528
+ async count(props) {
1529
+ countValuesSchema.parse(props);
1530
+ const count = (await this.listReferences(fileTypeSchema4.Enum.value, props.projectId)).length;
1531
+ return count;
1532
+ }
1533
+ /**
1534
+ * Checks if given object is of type Value
1535
+ */
1536
+ isValue(obj) {
1537
+ return valueFileSchema.safeParse(obj).success;
1538
+ }
1539
+ /**
1540
+ * Reads the given Value from disk and validates it against the ValueDefinition
1541
+ */
1542
+ async validate(props) {
1543
+ validateValueSchema.parse(props);
1544
+ const value = await this.read(props);
1545
+ const valueSchema = getValueSchemaFromDefinition(props.definition);
1546
+ return valueSchema.safeParse(value.content);
1547
+ }
1548
+ };
1549
+
1550
+ // src/service/EntryService.ts
1551
+ var EntryService = class extends AbstractCrudService {
1552
+ constructor(options, jsonFileService, gitService, collectionService, valueService) {
1553
+ super(serviceTypeSchema6.Enum.Entry, options);
1554
+ this.jsonFileService = jsonFileService;
1555
+ this.gitService = gitService;
1556
+ this.collectionService = collectionService;
1557
+ this.valueService = valueService;
1558
+ }
1559
+ /**
1560
+ * Creates a new Entry
1561
+ */
1562
+ async create(props) {
1563
+ createEntrySchema.parse(props);
1564
+ const id = uuid5();
1565
+ const projectPath = pathTo.project(props.projectId);
1566
+ const entryFilePath = pathTo.entryFile(
1567
+ props.projectId,
1568
+ props.collectionId,
1569
+ id,
1570
+ props.language
1571
+ );
1572
+ const collection = await this.collectionService.read({
1573
+ projectId: props.projectId,
1574
+ id: props.collectionId
1575
+ });
1576
+ await this.validateValueReferences(
1577
+ props.projectId,
1578
+ props.collectionId,
1579
+ props.valueReferences,
1580
+ collection.valueDefinitions
1581
+ );
1582
+ const entryFile = {
1583
+ fileType: "entry",
1584
+ id,
1585
+ language: props.language,
1586
+ valueReferences: props.valueReferences,
1587
+ created: currentTimestamp4()
1588
+ };
1589
+ await this.jsonFileService.create(
1590
+ entryFile,
1591
+ entryFilePath,
1592
+ entryFileSchema
1593
+ );
1594
+ await this.gitService.add(projectPath, [entryFilePath]);
1595
+ await this.gitService.commit(projectPath, this.gitMessage.create);
1596
+ return entryFile;
1597
+ }
1598
+ /**
1599
+ * Returns an Entry by ID and language
1600
+ */
1601
+ async read(props) {
1602
+ readEntrySchema.parse(props);
1603
+ const entryFile = await this.jsonFileService.read(
1604
+ pathTo.entryFile(
1605
+ props.projectId,
1606
+ props.collectionId,
1607
+ props.id,
1608
+ props.language
1609
+ ),
1610
+ entryFileSchema
1611
+ );
1612
+ return entryFile;
1613
+ }
1614
+ /**
1615
+ * Updates Entry with given ValueReferences
1616
+ */
1617
+ async update(props) {
1618
+ updateEntrySchema.parse(props);
1619
+ const projectPath = pathTo.project(props.projectId);
1620
+ const entryFilePath = pathTo.entryFile(
1621
+ props.projectId,
1622
+ props.collectionId,
1623
+ props.id,
1624
+ props.language
1625
+ );
1626
+ const collection = await this.collectionService.read({
1627
+ projectId: props.projectId,
1628
+ id: props.collectionId
1629
+ });
1630
+ await this.validateValueReferences(
1631
+ props.projectId,
1632
+ props.collectionId,
1633
+ props.valueReferences,
1634
+ collection.valueDefinitions
1635
+ );
1636
+ const prevEntryFile = await this.read({
1637
+ projectId: props.projectId,
1638
+ collectionId: props.collectionId,
1639
+ id: props.id,
1640
+ language: props.language
1641
+ });
1642
+ const entryFile = {
1643
+ ...prevEntryFile,
1644
+ valueReferences: props.valueReferences,
1645
+ updated: currentTimestamp4()
1646
+ };
1647
+ await this.jsonFileService.update(
1648
+ entryFile,
1649
+ entryFilePath,
1650
+ entryFileSchema
1651
+ );
1652
+ await this.gitService.add(projectPath, [entryFilePath]);
1653
+ await this.gitService.commit(projectPath, this.gitMessage.update);
1654
+ return entryFile;
1655
+ }
1656
+ /**
1657
+ * Deletes given Entry
1658
+ */
1659
+ async delete(props) {
1660
+ deleteEntrySchema.parse(props);
1661
+ const projectPath = pathTo.project(props.projectId);
1662
+ const entryFilePath = pathTo.entryFile(
1663
+ props.projectId,
1664
+ props.collectionId,
1665
+ props.id,
1666
+ props.language
1667
+ );
1668
+ await Fs6.remove(entryFilePath);
1669
+ await this.gitService.add(projectPath, [entryFilePath]);
1670
+ await this.gitService.commit(projectPath, this.gitMessage.delete);
1671
+ }
1672
+ async list(props) {
1673
+ listEntriesSchema.parse(props);
1674
+ const references = await this.listReferences(
1675
+ fileTypeSchema5.Enum.entry,
1676
+ props.projectId,
1677
+ props.collectionId
1678
+ );
1679
+ const list = await returnResolved(
1680
+ references.map((reference) => {
1681
+ if (!reference.language) {
1682
+ throw new RequiredParameterMissingError("language");
1683
+ }
1684
+ return this.read({
1685
+ projectId: props.projectId,
1686
+ collectionId: props.collectionId,
1687
+ id: reference.id,
1688
+ language: reference.language
1689
+ });
1690
+ })
1691
+ );
1692
+ return this.paginate(
1693
+ list,
1694
+ props.sort,
1695
+ props.filter,
1696
+ props.limit,
1697
+ props.offset
1698
+ );
1699
+ }
1700
+ async count(props) {
1701
+ countEntriesSchema.parse(props);
1702
+ return (await this.listReferences(
1703
+ fileTypeSchema5.Enum.entry,
1704
+ props.projectId,
1705
+ props.collectionId
1706
+ )).length;
1707
+ }
1708
+ /**
1709
+ * Checks if given object of Collection, CollectionItem,
1710
+ * Field, Project or Asset is of type CollectionItem
1711
+ */
1712
+ isEntry(obj) {
1713
+ return entrySchema.safeParse(obj).success;
1714
+ }
1715
+ /**
1716
+ * Validates referenced Values against the Collections definition
1717
+ *
1718
+ * @todo should probably return all errors occurring during parsing instead of throwing
1719
+ */
1720
+ async validateValueReferences(projectId, collectionId, valueReferences, valueDefinitions) {
1721
+ await Promise.all(
1722
+ valueReferences.map(async (reference) => {
1723
+ const definition = valueDefinitions.find((def) => {
1724
+ if (def.id === reference.definitionId) {
1725
+ return true;
1726
+ }
1727
+ return false;
1728
+ });
1729
+ if (!definition) {
1730
+ throw new Error(
1731
+ `No definition with ID "${reference.definitionId}" found in Collection "${collectionId}" for given Value reference`
1732
+ );
1733
+ }
1734
+ const schema = getValueSchemaFromDefinition2(definition);
1735
+ const value = await this.valueService.read({
1736
+ ...reference.references,
1737
+ projectId
1738
+ });
1739
+ schema.parse(value.content);
1740
+ })
1741
+ );
1742
+ }
1743
+ };
1744
+
1745
+ // src/service/ProjectService.ts
1746
+ import {
1747
+ createProjectSchema,
1748
+ currentTimestamp as currentTimestamp5,
1749
+ deleteProjectSchema,
1750
+ fileTypeSchema as fileTypeSchema6,
1751
+ gitCommitIconSchema as gitCommitIconSchema2,
1752
+ listProjectsSchema,
1753
+ projectFileSchema,
1754
+ projectFolderSchema as projectFolderSchema2,
1755
+ readProjectSchema,
1756
+ serviceTypeSchema as serviceTypeSchema8,
1757
+ updateProjectSchema,
1758
+ upgradeProjectSchema,
1759
+ uuid as uuid6
1760
+ } from "@elek-io/shared";
1761
+ import Fs7 from "fs-extra";
1762
+ import Os2 from "os";
1763
+ import Path2 from "path";
1764
+ import Semver from "semver";
1765
+
1766
+ // src/error/ProjectUpgradeError.ts
1767
+ var ProjectUpgradeError = class extends Error {
1768
+ constructor(message) {
1769
+ super(message);
1770
+ this.name = "ProjectUpgradeError";
1771
+ }
1772
+ };
1773
+
1774
+ // src/service/SearchService.ts
1775
+ import {
1776
+ serviceTypeSchema as serviceTypeSchema7
1777
+ } from "@elek-io/shared";
1778
+ var SearchService = class extends AbstractCrudService {
1779
+ constructor(options, assetService, collectionService) {
1780
+ super(serviceTypeSchema7.enum.Search, options);
1781
+ this.assetService = assetService;
1782
+ this.collectionService = collectionService;
1783
+ }
1784
+ /**
1785
+ * Search all models inside the project for given query
1786
+ *
1787
+ * @todo Implement SearchOptions parameter
1788
+ *
1789
+ * @param project Project to search in
1790
+ * @param query Query to search for
1791
+ */
1792
+ async search(projectId, query, fileType) {
1793
+ const results = [];
1794
+ const normalizedQuery = query.trim();
1795
+ if (normalizedQuery === "") {
1796
+ return results;
1797
+ }
1798
+ const paginatedLists = (await Promise.all([this.assetService.list({ projectId, filter: query })])).flat();
1799
+ paginatedLists.forEach((paginatedList) => {
1800
+ paginatedList.list.flat().forEach((file) => {
1801
+ const result = {
1802
+ id: file.id,
1803
+ language: file.language,
1804
+ name: file.name,
1805
+ type: file.fileType,
1806
+ matches: []
1807
+ };
1808
+ for (const [key, value] of Object.entries(file)) {
1809
+ const valueString = String(value);
1810
+ if (valueString.toLowerCase().includes(normalizedQuery.toLowerCase())) {
1811
+ const matchStart = valueString.toLowerCase().indexOf(normalizedQuery.toLowerCase());
1812
+ const matchEnd = matchStart + normalizedQuery.length;
1813
+ result.matches.push({
1814
+ key,
1815
+ prefix: this.truncate(
1816
+ valueString.substring(0, matchStart),
1817
+ "start"
1818
+ ),
1819
+ match: valueString.substring(matchStart, matchEnd),
1820
+ suffix: this.truncate(
1821
+ valueString.substring(matchEnd, valueString.length),
1822
+ "end"
1823
+ )
1824
+ });
1825
+ }
1826
+ }
1827
+ if (result.matches.length > 0) {
1828
+ results.push(result);
1829
+ }
1830
+ });
1831
+ });
1832
+ return results;
1833
+ }
1834
+ truncate(value, at, limit = 15) {
1835
+ if (at === "start") {
1836
+ return `${value.substring(value.length - limit, value.length)}`;
1837
+ } else {
1838
+ return `${value.substring(0, limit)}`;
1839
+ }
1840
+ }
1841
+ };
1842
+
1843
+ // src/service/ProjectService.ts
1844
+ var ProjectService = class extends AbstractCrudService {
1845
+ constructor(options, jsonFileService, userService, gitService, searchService, assetService, collectionService, entryService, valueService) {
1846
+ super(serviceTypeSchema8.Enum.Project, options);
1847
+ this.jsonFileService = jsonFileService;
1848
+ this.userService = userService;
1849
+ this.gitService = gitService;
1850
+ this.searchService = searchService;
1851
+ this.assetService = assetService;
1852
+ this.collectionService = collectionService;
1853
+ this.entryService = entryService;
1854
+ this.valueService = valueService;
1855
+ }
1856
+ /**
1857
+ * Creates a new Project
1858
+ */
1859
+ async create(props) {
1860
+ createProjectSchema.parse(props);
1861
+ const user = await this.userService.get();
1862
+ if (!user) {
1863
+ throw new NoCurrentUserError();
1864
+ }
1865
+ const id = uuid6();
1866
+ const defaultSettings = {
1867
+ locale: {
1868
+ default: user.locale,
1869
+ supported: [user.locale]
1870
+ }
1871
+ };
1872
+ const projectFile = {
1873
+ ...props,
1874
+ fileType: "project",
1875
+ id,
1876
+ description: props.description || "",
1877
+ settings: Object.assign({}, defaultSettings, props.settings),
1878
+ created: currentTimestamp5(),
1879
+ coreVersion: this.options.version,
1880
+ // @todo should be read from package.json to avoid duplicates
1881
+ status: "todo",
1882
+ version: "0.0.1"
1883
+ };
1884
+ const projectPath = pathTo.project(id);
1885
+ await Fs7.ensureDir(projectPath);
1886
+ try {
1887
+ await this.createFolderStructure(projectPath);
1888
+ await this.createGitignore(projectPath);
1889
+ await this.gitService.init(projectPath, { initialBranch: "main" });
1890
+ await this.jsonFileService.create(
1891
+ projectFile,
1892
+ pathTo.projectFile(id),
1893
+ projectFileSchema
1894
+ );
1895
+ await this.gitService.add(projectPath, ["."]);
1896
+ await this.gitService.commit(
1897
+ projectPath,
1898
+ `${gitCommitIconSchema2.enum.INIT} Created this new elek.io project`
1899
+ );
1900
+ await this.gitService.switch(projectPath, "stage", { isNew: true });
1901
+ } catch (error) {
1902
+ await this.delete({
1903
+ id
1904
+ });
1905
+ throw error;
1906
+ }
1907
+ return projectFile;
1908
+ }
1909
+ /**
1910
+ * Returns a Project by ID
1911
+ */
1912
+ async read(props) {
1913
+ readProjectSchema.parse(props);
1914
+ const projectFile = await this.jsonFileService.read(
1915
+ pathTo.projectFile(props.id),
1916
+ projectFileSchema
1917
+ );
1918
+ return projectFile;
1919
+ }
1920
+ /**
1921
+ * Updates given Project
1922
+ */
1923
+ async update(props) {
1924
+ updateProjectSchema.parse(props);
1925
+ const projectPath = pathTo.project(props.id);
1926
+ const filePath = pathTo.projectFile(props.id);
1927
+ const prevProjectFile = await this.read(props);
1928
+ const projectFile = {
1929
+ ...prevProjectFile,
1930
+ ...props,
1931
+ updated: currentTimestamp5()
1932
+ };
1933
+ await this.jsonFileService.update(projectFile, filePath, projectFileSchema);
1934
+ await this.gitService.add(projectPath, [filePath]);
1935
+ await this.gitService.commit(projectPath, this.gitMessage.update);
1936
+ return projectFile;
1937
+ }
1938
+ /**
1939
+ * Upgrades given Project to the latest version of this client
1940
+ *
1941
+ * Needed when a new core version is requiring changes to existing files or structure.
1942
+ *
1943
+ * @todo Find out why using this.snapshotService is throwing isObjWithKeyAndValueOfString of undefined error in gitService (maybe binding issue)
1944
+ */
1945
+ async upgrade(props) {
1946
+ upgradeProjectSchema.parse(props);
1947
+ const project = await this.read(props);
1948
+ const projectPath = pathTo.project(project.id);
1949
+ if (Semver.gt(project.coreVersion, this.options.version)) {
1950
+ throw new Error(
1951
+ `Failed upgrading project. The projects core version "${project.coreVersion}" is higher than the current core version "${this.options.version}" of this client. A client upgrade is needed beforehand.`
1952
+ );
1953
+ }
1954
+ if (Semver.eq(project.coreVersion, this.options.version)) {
1955
+ return;
1956
+ }
1957
+ const upgradeFiles = await files(
1958
+ Path2.resolve(__dirname, "../upgrade"),
1959
+ "ts"
1960
+ );
1961
+ const upgrades = (await Promise.all(
1962
+ upgradeFiles.map((file) => {
1963
+ return import(Path2.join("../upgrade", file.name));
1964
+ })
1965
+ )).map((upgradeImport) => {
1966
+ return upgradeImport.default;
1967
+ });
1968
+ const sortedUpgrades = upgrades.sort((a, b) => {
1969
+ return Semver.compare(a.to, b.to);
1970
+ }).filter((upgrade) => {
1971
+ if (upgrade.to !== "0.0.0") {
1972
+ return upgrade;
1973
+ }
1974
+ });
1975
+ for (let index = 0; index < sortedUpgrades.length; index++) {
1976
+ const upgrade = sortedUpgrades[index];
1977
+ if (!upgrade) {
1978
+ throw new Error("Expected ProjectUpgrade but got undefined");
1979
+ }
1980
+ const failsafeTag = await this.gitService.tags.create({
1981
+ path: projectPath,
1982
+ message: `Attempting to upgrade Project to Core version "${upgrade.to}"`
1983
+ });
1984
+ try {
1985
+ await upgrade.run(project);
1986
+ project.coreVersion = upgrade.to;
1987
+ await this.update(project);
1988
+ await this.gitService.tags.create({
1989
+ path: projectPath,
1990
+ message: `Upgraded Project to Core version "${upgrade.to}"`
1991
+ });
1992
+ await this.gitService.tags.delete({
1993
+ path: projectPath,
1994
+ id: failsafeTag.id
1995
+ });
1996
+ } catch (error) {
1997
+ await this.gitService.reset(projectPath, "hard", failsafeTag.id);
1998
+ throw new ProjectUpgradeError(
1999
+ `Failed to upgrade Project to Core version "${upgrade.to}"`
2000
+ );
2001
+ }
2002
+ }
2003
+ }
2004
+ /**
2005
+ * Deletes given Project
2006
+ *
2007
+ * Deletes the whole Project folder including the history, not only the config file.
2008
+ * Use with caution, since a Project that is only available locally could be lost forever.
2009
+ * Or changes that are not pushed to a remote yet, will be lost too.
2010
+ */
2011
+ async delete(props) {
2012
+ deleteProjectSchema.parse(props);
2013
+ await Fs7.remove(pathTo.project(props.id));
2014
+ }
2015
+ async list(props) {
2016
+ if (props) {
2017
+ listProjectsSchema.parse(props);
2018
+ }
2019
+ const references = await this.listReferences(fileTypeSchema6.Enum.project);
2020
+ const list = await returnResolved(
2021
+ references.map((reference) => {
2022
+ return this.read({ id: reference.id });
2023
+ })
2024
+ );
2025
+ return this.paginate(
2026
+ list,
2027
+ props?.sort,
2028
+ props?.filter,
2029
+ props?.limit,
2030
+ props?.offset
2031
+ );
2032
+ }
2033
+ async count() {
2034
+ return (await this.listReferences(fileTypeSchema6.Enum.project)).length;
2035
+ }
2036
+ /**
2037
+ * Search all models inside the project for given query
2038
+ *
2039
+ * @param projectId Project ID to search in
2040
+ * @param query Query to search for
2041
+ * @param type (Optional) specify the type to search for
2042
+ */
2043
+ async search(projectId, query, type) {
2044
+ return this.searchService.search(projectId, query, type);
2045
+ }
2046
+ /**
2047
+ * Checks if given object is of type Project
2048
+ */
2049
+ isProject(obj) {
2050
+ return projectFileSchema.safeParse(obj).success;
2051
+ }
2052
+ /**
2053
+ * Exports given Project to JSON
2054
+ *
2055
+ * @todo performance tests
2056
+ * @todo add progress callback
2057
+ */
2058
+ async exportToJson(projectId) {
2059
+ const project = await this.read({ id: projectId });
2060
+ const assets = (await this.assetService.list({ projectId, limit: 0 })).list;
2061
+ const collections = (await this.collectionService.list({ projectId, limit: 0 })).list;
2062
+ const collectionExport = await Promise.all(
2063
+ collections.map(async (collection) => {
2064
+ const entries = (await this.entryService.list({
2065
+ projectId,
2066
+ collectionId: collection.id,
2067
+ limit: 0
2068
+ })).list;
2069
+ const entryExport = await Promise.all(
2070
+ entries.map(async (entry) => {
2071
+ const valueExport = await Promise.all(
2072
+ entry.valueReferences.map(async (valueReference) => {
2073
+ return this.valueService.read({
2074
+ projectId,
2075
+ id: valueReference.references.id,
2076
+ language: valueReference.references.language
2077
+ });
2078
+ })
2079
+ );
2080
+ return {
2081
+ ...entry,
2082
+ values: valueExport
2083
+ };
2084
+ })
2085
+ );
2086
+ return {
2087
+ ...collection,
2088
+ entries: entryExport
2089
+ };
2090
+ })
2091
+ );
2092
+ return {
2093
+ ...project,
2094
+ assets,
2095
+ collections: collectionExport
2096
+ };
2097
+ }
2098
+ /**
2099
+ * Creates the projects folder structure and makes sure to
2100
+ * write empty .gitkeep files inside them to ensure they are
2101
+ * committed
2102
+ */
2103
+ async createFolderStructure(path) {
2104
+ const folders2 = Object.values(projectFolderSchema2.Values);
2105
+ await Promise.all(
2106
+ folders2.map(async (folder) => {
2107
+ await Fs7.mkdirp(Path2.join(path, folder));
2108
+ await Fs7.writeFile(Path2.join(path, folder, ".gitkeep"), "");
2109
+ })
2110
+ );
2111
+ }
2112
+ /**
2113
+ * Writes the Projects main .gitignore file to disk
2114
+ *
2115
+ * @todo Add general things to ignore
2116
+ * @see https://github.com/github/gitignore/tree/master/Global
2117
+ */
2118
+ async createGitignore(path) {
2119
+ const lines = [
2120
+ "# Ignore all hidden files and folders...",
2121
+ ".*",
2122
+ "# ...but these",
2123
+ "!/.gitignore",
2124
+ "!/.gitattributes",
2125
+ "!/**/.gitkeep",
2126
+ "",
2127
+ "# elek.io related ignores"
2128
+ // projectFolderSchema.Enum.theme + '/',
2129
+ // projectFolderSchema.Enum.public + '/',
2130
+ // projectFolderSchema.Enum.logs + '/',
2131
+ ];
2132
+ await Fs7.writeFile(Path2.join(path, ".gitignore"), lines.join(Os2.EOL));
2133
+ }
2134
+ };
2135
+
2136
+ // src/index.ts
2137
+ var ElekIoCore = class {
2138
+ constructor(props) {
2139
+ if (props) {
2140
+ constructorElekIoCoreSchema.parse(props);
2141
+ }
2142
+ const environment = environmentSchema.parse(process.env.NODE_ENV);
2143
+ const defaults = {
2144
+ environment,
2145
+ version: "0.0.0",
2146
+ file: {
2147
+ json: {
2148
+ indentation: 2
2149
+ }
2150
+ }
2151
+ };
2152
+ this.options = Object.assign({}, defaults, props);
2153
+ this.jsonFileService = new JsonFileService(this.options);
2154
+ this.userService = new UserService(this.jsonFileService);
2155
+ this.gitService = new GitService2(this.options, this.userService);
2156
+ this.assetService = new AssetService(
2157
+ this.options,
2158
+ this.jsonFileService,
2159
+ this.gitService
2160
+ );
2161
+ this.valueService = new ValueService(
2162
+ this.options,
2163
+ this.jsonFileService,
2164
+ this.gitService,
2165
+ this.assetService
2166
+ );
2167
+ this.collectionService = new CollectionService(
2168
+ this.options,
2169
+ this.jsonFileService,
2170
+ this.gitService
2171
+ );
2172
+ this.entryService = new EntryService(
2173
+ this.options,
2174
+ this.jsonFileService,
2175
+ this.gitService,
2176
+ this.collectionService,
2177
+ this.valueService
2178
+ );
2179
+ this.searchService = new SearchService(
2180
+ this.options,
2181
+ this.assetService,
2182
+ this.collectionService
2183
+ );
2184
+ this.projectService = new ProjectService(
2185
+ this.options,
2186
+ this.jsonFileService,
2187
+ this.userService,
2188
+ this.gitService,
2189
+ this.searchService,
2190
+ this.assetService,
2191
+ this.collectionService,
2192
+ this.entryService,
2193
+ this.valueService
2194
+ );
2195
+ if (environment !== "production") {
2196
+ console.info(`Initializing inside an "${environment}" environment`, {
2197
+ options: this.options
2198
+ });
2199
+ }
2200
+ Fs8.mkdirpSync(pathTo.projects);
2201
+ Fs8.mkdirpSync(pathTo.tmp);
2202
+ Fs8.emptyDirSync(pathTo.tmp);
2203
+ }
2204
+ /**
2205
+ * Utility / helper functions
2206
+ */
2207
+ get util() {
2208
+ return util_exports;
2209
+ }
2210
+ /**
2211
+ *
2212
+ */
2213
+ get user() {
2214
+ return this.userService;
2215
+ }
2216
+ /**
2217
+ * CRUD methods to work with Projects
2218
+ */
2219
+ get projects() {
2220
+ return this.projectService;
2221
+ }
2222
+ /**
2223
+ * CRUD methods to work with Assets
2224
+ */
2225
+ get assets() {
2226
+ return this.assetService;
2227
+ }
2228
+ /**
2229
+ * CRUD methods to work with Collections
2230
+ */
2231
+ get collections() {
2232
+ return this.collectionService;
2233
+ }
2234
+ /**
2235
+ * CRUD methods to work with Entries
2236
+ */
2237
+ get entries() {
2238
+ return this.entryService;
2239
+ }
2240
+ /**
2241
+ * CRUD methods to work with Values
2242
+ */
2243
+ get values() {
2244
+ return this.valueService;
2245
+ }
2246
+ };
2247
+ export {
2248
+ ElekIoCore as default
2249
+ };
2250
+ //# sourceMappingURL=index.js.map