@s-hirano-ist/s-scripts 1.26.8 → 4.0.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.
Files changed (180) hide show
  1. package/README.md +2 -0
  2. package/dist/chunk-5OXUZW2Q.js +590 -0
  3. package/dist/chunk-7PVU2Z32.js +71 -0
  4. package/dist/chunk-AJMTRCPJ.js +15 -0
  5. package/dist/chunk-AZF2R2BF.js +85 -0
  6. package/dist/chunk-F4J6JBR4.js +115 -0
  7. package/dist/chunk-HDYQV2Y2.js +58 -0
  8. package/dist/chunk-HIK7SX34.js +85 -0
  9. package/dist/chunk-IWNZQXDG.js +7660 -0
  10. package/dist/chunk-J4VLH4VA.js +267 -0
  11. package/dist/chunk-JZOR5GA2.js +42 -0
  12. package/dist/chunk-KKZP7ZF3.js +77 -0
  13. package/dist/chunk-N6TZF755.js +102 -0
  14. package/dist/chunk-S5WJ5VXW.js +85 -0
  15. package/dist/chunk-UXGLXHQ6.js +40 -0
  16. package/dist/cleanup-minio-images.js +115 -92
  17. package/dist/enrich-books.js +87 -78
  18. package/dist/fetch-articles.js +101 -90
  19. package/dist/fetch-books.js +147 -128
  20. package/dist/fetch-images.js +88 -72
  21. package/dist/fetch-notes.js +66 -54
  22. package/dist/find-duplicate-json-articles.js +100 -97
  23. package/dist/fix-book-frontmatter.js +111 -99
  24. package/dist/fix-image-formats.js +81 -65
  25. package/dist/ingest-articles.js +30 -249
  26. package/dist/ingest-books.js +61 -318
  27. package/dist/ingest-images.js +33 -184
  28. package/dist/ingest-notes.js +41 -171
  29. package/dist/rag/delete-collection.js +32 -22
  30. package/dist/rag/ingest.js +371 -92
  31. package/dist/rag/search.js +80 -76
  32. package/dist/reset-articles.js +57 -60
  33. package/dist/reset-books.js +57 -59
  34. package/dist/reset-images.js +57 -59
  35. package/dist/reset-notes.js +57 -59
  36. package/dist/revert-articles.js +53 -55
  37. package/dist/revert-books.js +53 -53
  38. package/dist/revert-images.js +53 -53
  39. package/dist/revert-notes.js +53 -53
  40. package/dist/s-content-format.js +103 -13
  41. package/dist/update-json-articles.js +101 -97
  42. package/dist/update-raw-articles.js +130 -130
  43. package/dist/validate-json-articles.js +129 -130
  44. package/package.json +13 -50
  45. package/dist/cleanup-minio-images.d.ts +0 -3
  46. package/dist/cleanup-minio-images.d.ts.map +0 -1
  47. package/dist/cleanup-minio-images.js.map +0 -1
  48. package/dist/enrich-books.d.ts +0 -3
  49. package/dist/enrich-books.d.ts.map +0 -1
  50. package/dist/enrich-books.js.map +0 -1
  51. package/dist/fetch-articles.d.ts +0 -3
  52. package/dist/fetch-articles.d.ts.map +0 -1
  53. package/dist/fetch-articles.js.map +0 -1
  54. package/dist/fetch-books.d.ts +0 -3
  55. package/dist/fetch-books.d.ts.map +0 -1
  56. package/dist/fetch-books.js.map +0 -1
  57. package/dist/fetch-images.d.ts +0 -3
  58. package/dist/fetch-images.d.ts.map +0 -1
  59. package/dist/fetch-images.js.map +0 -1
  60. package/dist/fetch-notes.d.ts +0 -3
  61. package/dist/fetch-notes.d.ts.map +0 -1
  62. package/dist/fetch-notes.js.map +0 -1
  63. package/dist/find-duplicate-json-articles.d.ts +0 -3
  64. package/dist/find-duplicate-json-articles.d.ts.map +0 -1
  65. package/dist/find-duplicate-json-articles.js.map +0 -1
  66. package/dist/fix-book-frontmatter.d.ts +0 -3
  67. package/dist/fix-book-frontmatter.d.ts.map +0 -1
  68. package/dist/fix-book-frontmatter.js.map +0 -1
  69. package/dist/fix-image-formats.d.ts +0 -3
  70. package/dist/fix-image-formats.d.ts.map +0 -1
  71. package/dist/fix-image-formats.js.map +0 -1
  72. package/dist/infrastructures/articles-command-repository.d.ts +0 -57
  73. package/dist/infrastructures/articles-command-repository.d.ts.map +0 -1
  74. package/dist/infrastructures/articles-command-repository.js +0 -68
  75. package/dist/infrastructures/articles-command-repository.js.map +0 -1
  76. package/dist/infrastructures/books-command-repository.d.ts +0 -34
  77. package/dist/infrastructures/books-command-repository.d.ts.map +0 -1
  78. package/dist/infrastructures/books-command-repository.js +0 -53
  79. package/dist/infrastructures/books-command-repository.js.map +0 -1
  80. package/dist/infrastructures/cache-invalidation-client.d.ts +0 -9
  81. package/dist/infrastructures/cache-invalidation-client.d.ts.map +0 -1
  82. package/dist/infrastructures/cache-invalidation-client.js +0 -21
  83. package/dist/infrastructures/cache-invalidation-client.js.map +0 -1
  84. package/dist/infrastructures/cache-invalidation-client.test.d.ts +0 -2
  85. package/dist/infrastructures/cache-invalidation-client.test.d.ts.map +0 -1
  86. package/dist/infrastructures/cache-invalidation-client.test.js +0 -25
  87. package/dist/infrastructures/cache-invalidation-client.test.js.map +0 -1
  88. package/dist/infrastructures/images-command-repository.d.ts +0 -34
  89. package/dist/infrastructures/images-command-repository.d.ts.map +0 -1
  90. package/dist/infrastructures/images-command-repository.js +0 -53
  91. package/dist/infrastructures/images-command-repository.js.map +0 -1
  92. package/dist/infrastructures/notes-command-repository.d.ts +0 -34
  93. package/dist/infrastructures/notes-command-repository.d.ts.map +0 -1
  94. package/dist/infrastructures/notes-command-repository.js +0 -53
  95. package/dist/infrastructures/notes-command-repository.js.map +0 -1
  96. package/dist/ingest-articles.d.ts +0 -3
  97. package/dist/ingest-articles.d.ts.map +0 -1
  98. package/dist/ingest-articles.js.map +0 -1
  99. package/dist/ingest-books.d.ts +0 -3
  100. package/dist/ingest-books.d.ts.map +0 -1
  101. package/dist/ingest-books.js.map +0 -1
  102. package/dist/ingest-images.d.ts +0 -3
  103. package/dist/ingest-images.d.ts.map +0 -1
  104. package/dist/ingest-images.js.map +0 -1
  105. package/dist/ingest-notes.d.ts +0 -3
  106. package/dist/ingest-notes.d.ts.map +0 -1
  107. package/dist/ingest-notes.js.map +0 -1
  108. package/dist/lib/glob-paths.d.ts +0 -6
  109. package/dist/lib/glob-paths.d.ts.map +0 -1
  110. package/dist/lib/glob-paths.js +0 -9
  111. package/dist/lib/glob-paths.js.map +0 -1
  112. package/dist/lib/glob-paths.test.d.ts +0 -2
  113. package/dist/lib/glob-paths.test.d.ts.map +0 -1
  114. package/dist/lib/glob-paths.test.js +0 -30
  115. package/dist/lib/glob-paths.test.js.map +0 -1
  116. package/dist/lib/html-charset.d.ts +0 -4
  117. package/dist/lib/html-charset.d.ts.map +0 -1
  118. package/dist/lib/html-charset.js +0 -33
  119. package/dist/lib/html-charset.js.map +0 -1
  120. package/dist/lib/html-charset.test.d.ts +0 -2
  121. package/dist/lib/html-charset.test.d.ts.map +0 -1
  122. package/dist/lib/html-charset.test.js +0 -27
  123. package/dist/lib/html-charset.test.js.map +0 -1
  124. package/dist/rag/delete-collection.d.ts +0 -3
  125. package/dist/rag/delete-collection.d.ts.map +0 -1
  126. package/dist/rag/delete-collection.js.map +0 -1
  127. package/dist/rag/ingest-config.d.ts +0 -8
  128. package/dist/rag/ingest-config.d.ts.map +0 -1
  129. package/dist/rag/ingest-config.js +0 -8
  130. package/dist/rag/ingest-config.js.map +0 -1
  131. package/dist/rag/ingest.d.ts +0 -3
  132. package/dist/rag/ingest.d.ts.map +0 -1
  133. package/dist/rag/ingest.js.map +0 -1
  134. package/dist/rag/search.d.ts +0 -3
  135. package/dist/rag/search.d.ts.map +0 -1
  136. package/dist/rag/search.js.map +0 -1
  137. package/dist/reset-articles.d.ts +0 -3
  138. package/dist/reset-articles.d.ts.map +0 -1
  139. package/dist/reset-articles.js.map +0 -1
  140. package/dist/reset-books.d.ts +0 -3
  141. package/dist/reset-books.d.ts.map +0 -1
  142. package/dist/reset-books.js.map +0 -1
  143. package/dist/reset-images.d.ts +0 -3
  144. package/dist/reset-images.d.ts.map +0 -1
  145. package/dist/reset-images.js.map +0 -1
  146. package/dist/reset-notes.d.ts +0 -3
  147. package/dist/reset-notes.d.ts.map +0 -1
  148. package/dist/reset-notes.js.map +0 -1
  149. package/dist/revert-articles.d.ts +0 -3
  150. package/dist/revert-articles.d.ts.map +0 -1
  151. package/dist/revert-articles.js.map +0 -1
  152. package/dist/revert-books.d.ts +0 -3
  153. package/dist/revert-books.d.ts.map +0 -1
  154. package/dist/revert-books.js.map +0 -1
  155. package/dist/revert-images.d.ts +0 -3
  156. package/dist/revert-images.d.ts.map +0 -1
  157. package/dist/revert-images.js.map +0 -1
  158. package/dist/revert-notes.d.ts +0 -3
  159. package/dist/revert-notes.d.ts.map +0 -1
  160. package/dist/revert-notes.js.map +0 -1
  161. package/dist/s-content-format-runner.d.ts +0 -10
  162. package/dist/s-content-format-runner.d.ts.map +0 -1
  163. package/dist/s-content-format-runner.js +0 -76
  164. package/dist/s-content-format-runner.js.map +0 -1
  165. package/dist/s-content-format.d.ts +0 -3
  166. package/dist/s-content-format.d.ts.map +0 -1
  167. package/dist/s-content-format.js.map +0 -1
  168. package/dist/s-content-format.test.d.ts +0 -2
  169. package/dist/s-content-format.test.d.ts.map +0 -1
  170. package/dist/s-content-format.test.js +0 -99
  171. package/dist/s-content-format.test.js.map +0 -1
  172. package/dist/update-json-articles.d.ts +0 -3
  173. package/dist/update-json-articles.d.ts.map +0 -1
  174. package/dist/update-json-articles.js.map +0 -1
  175. package/dist/update-raw-articles.d.ts +0 -3
  176. package/dist/update-raw-articles.d.ts.map +0 -1
  177. package/dist/update-raw-articles.js.map +0 -1
  178. package/dist/validate-json-articles.d.ts +0 -3
  179. package/dist/validate-json-articles.d.ts.map +0 -1
  180. package/dist/validate-json-articles.js.map +0 -1
@@ -0,0 +1,85 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+ import {
3
+ makeExportedAt,
4
+ withTransactionRetry
5
+ } from "./chunk-IWNZQXDG.js";
6
+
7
+ // ../application/dist/batch/books-batch-service.js
8
+ var BooksBatchApplicationService = class {
9
+ commandRepository;
10
+ constructor(commandRepository) {
11
+ this.commandRepository = commandRepository;
12
+ }
13
+ /**
14
+ * Resets books for a new batch export.
15
+ *
16
+ * @remarks
17
+ * Delegates to repository's resetStatus which performs two operations
18
+ * atomically within a single transaction.
19
+ */
20
+ async resetBooks(userId) {
21
+ return this.commandRepository.resetStatus(userId, makeExportedAt());
22
+ }
23
+ /**
24
+ * Reverts books from LAST_UPDATED back to UNEXPORTED.
25
+ */
26
+ async revertBooks(userId) {
27
+ return this.commandRepository.bulkUpdateStatus({
28
+ userId,
29
+ fromStatus: "LAST_UPDATED",
30
+ toStatus: "UNEXPORTED"
31
+ });
32
+ }
33
+ };
34
+
35
+ // ../infrastructure/dist/batch/books-command-repository.js
36
+ function createBooksCommandRepository(prisma) {
37
+ return {
38
+ async bulkUpdateStatus(params) {
39
+ const { userId, fromStatus, toStatus, exportedAt } = params;
40
+ const result = await prisma.book.updateMany({
41
+ where: {
42
+ userId,
43
+ status: fromStatus
44
+ },
45
+ data: {
46
+ status: toStatus,
47
+ ...exportedAt && { exportedAt }
48
+ }
49
+ });
50
+ return { count: result.count };
51
+ },
52
+ async resetStatus(userId, exportedAt) {
53
+ const [finalized, marked] = await withTransactionRetry(() => prisma.$transaction([
54
+ prisma.book.updateMany({
55
+ where: {
56
+ userId,
57
+ status: "LAST_UPDATED"
58
+ },
59
+ data: {
60
+ status: "EXPORTED",
61
+ exportedAt
62
+ }
63
+ }),
64
+ prisma.book.updateMany({
65
+ where: {
66
+ userId,
67
+ status: "UNEXPORTED"
68
+ },
69
+ data: {
70
+ status: "LAST_UPDATED"
71
+ }
72
+ })
73
+ ]));
74
+ return {
75
+ finalized: { count: finalized.count },
76
+ marked: { count: marked.count }
77
+ };
78
+ }
79
+ };
80
+ }
81
+
82
+ export {
83
+ BooksBatchApplicationService,
84
+ createBooksCommandRepository
85
+ };
@@ -0,0 +1,115 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+
3
+ // ../storage/dist/src/client.js
4
+ import { Client } from "minio";
5
+
6
+ // ../storage/dist/src/cf-access-transport.js
7
+ import * as https from "node:https";
8
+ function createCfAccessTransport(config) {
9
+ if (!config?.clientId || !config.clientSecret) {
10
+ return void 0;
11
+ }
12
+ return {
13
+ request(options, ...rest) {
14
+ if (typeof options === "object" && !(options instanceof URL)) {
15
+ const headers = options.headers ?? {};
16
+ headers["CF-Access-Client-Id"] = config.clientId;
17
+ headers["CF-Access-Client-Secret"] = config.clientSecret;
18
+ options.headers = headers;
19
+ }
20
+ const request2 = https.request;
21
+ return request2(options, ...rest);
22
+ }
23
+ };
24
+ }
25
+
26
+ // ../storage/dist/src/client.js
27
+ function createMinioClient(config, cfAccess) {
28
+ const resolved = config ?? {
29
+ endPoint: process.env.MINIO_HOST ?? "",
30
+ port: Number(process.env.MINIO_PORT),
31
+ useSSL: process.env.MINIO_USE_SSL === void 0 ? !["localhost", "127.0.0.1"].includes(process.env.MINIO_HOST ?? "") : process.env.MINIO_USE_SSL === "true",
32
+ accessKey: process.env.MINIO_ACCESS_KEY ?? "",
33
+ secretKey: process.env.MINIO_SECRET_KEY ?? ""
34
+ };
35
+ const resolvedCfAccess = cfAccess ?? {
36
+ clientId: process.env.CF_ACCESS_CLIENT_ID ?? "",
37
+ clientSecret: process.env.CF_ACCESS_CLIENT_SECRET ?? ""
38
+ };
39
+ const transport = resolved.useSSL ? createCfAccessTransport(resolvedCfAccess) : void 0;
40
+ if (resolved.useSSL && !transport) {
41
+ throw new Error("CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET are required when useSSL is true.");
42
+ }
43
+ return new Client({
44
+ endPoint: resolved.endPoint,
45
+ port: resolved.port,
46
+ useSSL: resolved.useSSL,
47
+ accessKey: resolved.accessKey,
48
+ secretKey: resolved.secretKey,
49
+ ...transport && { transport }
50
+ });
51
+ }
52
+
53
+ // ../storage/dist/index.js
54
+ import { S3Error } from "minio";
55
+
56
+ // ../storage/dist/src/errors.js
57
+ var StorageOperationError = class extends Error {
58
+ context;
59
+ constructor(context, cause) {
60
+ super(`Storage ${context.operation} failed for ${context.objectKey}`, {
61
+ cause
62
+ });
63
+ this.name = "StorageOperationError";
64
+ this.context = context;
65
+ }
66
+ };
67
+
68
+ // ../storage/dist/src/storage-service.js
69
+ function createStorageService(client, bucketName, pathConfig) {
70
+ function buildObjectKey(path, isThumbnail) {
71
+ return `${isThumbnail ? pathConfig.thumbnailPrefix : pathConfig.originalPrefix}/${path}`;
72
+ }
73
+ function wrapStorageError(operation, objectKey, isThumbnail, error) {
74
+ throw new StorageOperationError({ operation, objectKey, bucketName, isThumbnail }, error);
75
+ }
76
+ return {
77
+ async uploadImage(path, bytes, isThumbnail) {
78
+ const objKey = buildObjectKey(path, isThumbnail);
79
+ try {
80
+ await client.putObject(bucketName, objKey, Buffer.from(bytes));
81
+ } catch (error) {
82
+ wrapStorageError("uploadImage", objKey, isThumbnail, error);
83
+ }
84
+ },
85
+ async getImage(path, isThumbnail) {
86
+ const objKey = buildObjectKey(path, isThumbnail);
87
+ try {
88
+ return await client.getObject(bucketName, objKey);
89
+ } catch (error) {
90
+ return wrapStorageError("getImage", objKey, isThumbnail, error);
91
+ }
92
+ },
93
+ async getImageOrThrow(path, isThumbnail) {
94
+ const objKey = buildObjectKey(path, isThumbnail);
95
+ try {
96
+ await client.statObject(bucketName, objKey);
97
+ } catch (error) {
98
+ wrapStorageError("getImageOrThrow", objKey, isThumbnail, error);
99
+ }
100
+ },
101
+ async deleteImage(path, isThumbnail) {
102
+ const objKey = buildObjectKey(path, isThumbnail);
103
+ try {
104
+ await client.removeObject(bucketName, objKey);
105
+ } catch (error) {
106
+ wrapStorageError("deleteImage", objKey, isThumbnail, error);
107
+ }
108
+ }
109
+ };
110
+ }
111
+
112
+ export {
113
+ createMinioClient,
114
+ createStorageService
115
+ };
@@ -0,0 +1,58 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+
3
+ // ../search/dist/frontmatter.js
4
+ import { load } from "js-yaml";
5
+ var BYTE_ORDER_MARK = "\uFEFF";
6
+ var OPENING_DELIMITER_REGEX = /^---[\t ]*\r?\n/u;
7
+ var CLOSING_DELIMITER_REGEX = /^---[\t ]*\r?$/gmu;
8
+ function stripByteOrderMark(source) {
9
+ return source.startsWith(BYTE_ORDER_MARK) ? source.slice(1) : source;
10
+ }
11
+ function getContentStart(source, delimiterEnd) {
12
+ if (source.startsWith("\r\n", delimiterEnd)) {
13
+ return delimiterEnd + 2;
14
+ }
15
+ if (source.startsWith("\n", delimiterEnd)) {
16
+ return delimiterEnd + 1;
17
+ }
18
+ return delimiterEnd;
19
+ }
20
+ function parseYamlObject(source) {
21
+ if (source.trim() === "") {
22
+ return {};
23
+ }
24
+ const parsed = load(source);
25
+ if (parsed === void 0) {
26
+ return {};
27
+ }
28
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
29
+ throw new Error("Markdown frontmatter must be a YAML object.");
30
+ }
31
+ return parsed;
32
+ }
33
+ function parseMarkdownFrontmatter(source) {
34
+ const normalizedSource = stripByteOrderMark(source);
35
+ const openingDelimiter = OPENING_DELIMITER_REGEX.exec(normalizedSource);
36
+ if (!openingDelimiter) {
37
+ if (/^---[\t ]*$/u.test(normalizedSource)) {
38
+ throw new Error("Markdown frontmatter is missing a closing delimiter.");
39
+ }
40
+ return { content: normalizedSource, data: {} };
41
+ }
42
+ CLOSING_DELIMITER_REGEX.lastIndex = openingDelimiter[0].length;
43
+ const closingDelimiter = CLOSING_DELIMITER_REGEX.exec(normalizedSource);
44
+ CLOSING_DELIMITER_REGEX.lastIndex = 0;
45
+ if (!closingDelimiter) {
46
+ throw new Error("Markdown frontmatter is missing a closing delimiter.");
47
+ }
48
+ const yamlSource = normalizedSource.slice(openingDelimiter[0].length, closingDelimiter.index);
49
+ const contentStart = getContentStart(normalizedSource, closingDelimiter.index + closingDelimiter[0].length);
50
+ return {
51
+ content: normalizedSource.slice(contentStart),
52
+ data: parseYamlObject(yamlSource)
53
+ };
54
+ }
55
+
56
+ export {
57
+ parseMarkdownFrontmatter
58
+ };
@@ -0,0 +1,85 @@
1
+ import { createRequire as __cliCreateRequire } from "node:module"; const require = __cliCreateRequire(import.meta.url);
2
+ import {
3
+ makeExportedAt,
4
+ withTransactionRetry
5
+ } from "./chunk-IWNZQXDG.js";
6
+
7
+ // ../application/dist/batch/images-batch-service.js
8
+ var ImagesBatchApplicationService = class {
9
+ commandRepository;
10
+ constructor(commandRepository) {
11
+ this.commandRepository = commandRepository;
12
+ }
13
+ /**
14
+ * Resets images for a new batch export.
15
+ *
16
+ * @remarks
17
+ * Delegates to repository's resetStatus which performs two operations
18
+ * atomically within a single transaction.
19
+ */
20
+ async resetImages(userId) {
21
+ return this.commandRepository.resetStatus(userId, makeExportedAt());
22
+ }
23
+ /**
24
+ * Reverts images from LAST_UPDATED back to UNEXPORTED.
25
+ */
26
+ async revertImages(userId) {
27
+ return this.commandRepository.bulkUpdateStatus({
28
+ userId,
29
+ fromStatus: "LAST_UPDATED",
30
+ toStatus: "UNEXPORTED"
31
+ });
32
+ }
33
+ };
34
+
35
+ // ../infrastructure/dist/batch/images-command-repository.js
36
+ function createImagesCommandRepository(prisma) {
37
+ return {
38
+ async bulkUpdateStatus(params) {
39
+ const { userId, fromStatus, toStatus, exportedAt } = params;
40
+ const result = await prisma.image.updateMany({
41
+ where: {
42
+ userId,
43
+ status: fromStatus
44
+ },
45
+ data: {
46
+ status: toStatus,
47
+ ...exportedAt && { exportedAt }
48
+ }
49
+ });
50
+ return { count: result.count };
51
+ },
52
+ async resetStatus(userId, exportedAt) {
53
+ const [finalized, marked] = await withTransactionRetry(() => prisma.$transaction([
54
+ prisma.image.updateMany({
55
+ where: {
56
+ userId,
57
+ status: "LAST_UPDATED"
58
+ },
59
+ data: {
60
+ status: "EXPORTED",
61
+ exportedAt
62
+ }
63
+ }),
64
+ prisma.image.updateMany({
65
+ where: {
66
+ userId,
67
+ status: "UNEXPORTED"
68
+ },
69
+ data: {
70
+ status: "LAST_UPDATED"
71
+ }
72
+ })
73
+ ]));
74
+ return {
75
+ finalized: { count: finalized.count },
76
+ marked: { count: marked.count }
77
+ };
78
+ }
79
+ };
80
+ }
81
+
82
+ export {
83
+ ImagesBatchApplicationService,
84
+ createImagesCommandRepository
85
+ };