@helloao/cli 0.0.5 → 0.0.6
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/README.md +125 -126
- package/actions.d.ts +64 -0
- package/actions.js +159 -43
- package/cli.js +88 -31
- package/db.d.ts +22 -9
- package/db.js +155 -154
- package/files.d.ts +7 -2
- package/files.js +29 -8
- package/migrations/20240623183848_add_book_order/migration.sql +26 -26
- package/migrations/20240629194121_add_chapter_links/migration.sql +45 -45
- package/migrations/20240629194513_add_chapter_content/migration.sql +30 -30
- package/migrations/20240705221833_remove_unused_columns/migration.sql +27 -27
- package/package.json +5 -2
- package/prisma-gen/edge.js +5 -5
- package/prisma-gen/index.js +7 -7
- package/s3.d.ts +32 -2
- package/s3.js +105 -12
- package/schema.prisma +154 -154
- package/uploads.d.ts +15 -5
- package/uploads.js +9 -89
package/schema.prisma
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
datasource db {
|
|
2
|
-
provider = "sqlite"
|
|
3
|
-
url = "file:../../bible-api.dev.db"
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
generator client {
|
|
7
|
-
provider = "prisma-client-js"
|
|
8
|
-
output = "./prisma-gen"
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
model Translation {
|
|
12
|
-
id String @id
|
|
13
|
-
name String
|
|
14
|
-
website String
|
|
15
|
-
licenseUrl String
|
|
16
|
-
shortName String?
|
|
17
|
-
englishName String
|
|
18
|
-
language String
|
|
19
|
-
textDirection String
|
|
20
|
-
|
|
21
|
-
// The SHA-256 hash of the translation
|
|
22
|
-
// includes everything about the translation, including the books, chapters, verses, footnotes, etc.
|
|
23
|
-
sha256 String?
|
|
24
|
-
|
|
25
|
-
books Book[]
|
|
26
|
-
chapters Chapter[]
|
|
27
|
-
verses ChapterVerse[]
|
|
28
|
-
footnotes ChapterFootnote[]
|
|
29
|
-
audioUrls ChapterAudioUrl[]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
model InputFile {
|
|
33
|
-
// The ID of the translation that the file is for
|
|
34
|
-
translationId String
|
|
35
|
-
|
|
36
|
-
// The name of the file
|
|
37
|
-
name String
|
|
38
|
-
|
|
39
|
-
format String
|
|
40
|
-
|
|
41
|
-
// The SHA-256 hash of the file
|
|
42
|
-
sha256 String
|
|
43
|
-
|
|
44
|
-
sizeInBytes Int
|
|
45
|
-
|
|
46
|
-
@@id([translationId, name])
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
model Book {
|
|
50
|
-
id String
|
|
51
|
-
|
|
52
|
-
translationId String
|
|
53
|
-
translation Translation @relation(fields: [translationId], references: [id])
|
|
54
|
-
|
|
55
|
-
name String
|
|
56
|
-
commonName String
|
|
57
|
-
title String?
|
|
58
|
-
order Int
|
|
59
|
-
|
|
60
|
-
numberOfChapters Int
|
|
61
|
-
|
|
62
|
-
// The SHA-256 hash of the book
|
|
63
|
-
sha256 String?
|
|
64
|
-
|
|
65
|
-
chapters Chapter[]
|
|
66
|
-
verses ChapterVerse[]
|
|
67
|
-
footnotes ChapterFootnote[]
|
|
68
|
-
audioUrls ChapterAudioUrl[]
|
|
69
|
-
|
|
70
|
-
@@id([translationId, id])
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
model Chapter {
|
|
74
|
-
number Int
|
|
75
|
-
|
|
76
|
-
bookId String
|
|
77
|
-
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
78
|
-
|
|
79
|
-
translationId String
|
|
80
|
-
translation Translation @relation(fields: [translationId], references: [id])
|
|
81
|
-
|
|
82
|
-
json String // The JSON of the chapter
|
|
83
|
-
|
|
84
|
-
// The SHA-256 hash of the chapter
|
|
85
|
-
sha256 String?
|
|
86
|
-
|
|
87
|
-
verses ChapterVerse[]
|
|
88
|
-
footnotes ChapterFootnote[]
|
|
89
|
-
audioUrls ChapterAudioUrl[]
|
|
90
|
-
|
|
91
|
-
@@id([translationId, bookId, number])
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
model ChapterAudioUrl {
|
|
95
|
-
number Int
|
|
96
|
-
bookId String
|
|
97
|
-
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
98
|
-
|
|
99
|
-
translationId String
|
|
100
|
-
translation Translation @relation(fields: [translationId], references: [id])
|
|
101
|
-
|
|
102
|
-
chapter Chapter @relation(fields: [translationId, bookId, number], references: [translationId, bookId, number])
|
|
103
|
-
|
|
104
|
-
reader String
|
|
105
|
-
url String
|
|
106
|
-
|
|
107
|
-
@@id([translationId, bookId, number, reader])
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
model ChapterVerse {
|
|
111
|
-
number Int
|
|
112
|
-
|
|
113
|
-
chapterNumber Int
|
|
114
|
-
chapter Chapter @relation(fields: [translationId, bookId, chapterNumber], references: [translationId, bookId, number])
|
|
115
|
-
|
|
116
|
-
bookId String
|
|
117
|
-
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
118
|
-
|
|
119
|
-
translationId String
|
|
120
|
-
translation Translation @relation(fields: [translationId], references: [id])
|
|
121
|
-
|
|
122
|
-
text String // The text of the verse
|
|
123
|
-
contentJson String // The JSON of the verse content
|
|
124
|
-
|
|
125
|
-
// The SHA-256 hash of the verse
|
|
126
|
-
sha256 String?
|
|
127
|
-
|
|
128
|
-
footnotes ChapterFootnote[]
|
|
129
|
-
|
|
130
|
-
@@id([translationId, bookId, chapterNumber, number])
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
model ChapterFootnote {
|
|
134
|
-
id Int
|
|
135
|
-
|
|
136
|
-
chapterNumber Int
|
|
137
|
-
chapter Chapter @relation(fields: [translationId, bookId, chapterNumber], references: [translationId, bookId, number])
|
|
138
|
-
|
|
139
|
-
bookId String
|
|
140
|
-
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
141
|
-
|
|
142
|
-
translationId String
|
|
143
|
-
translation Translation @relation(fields: [translationId], references: [id])
|
|
144
|
-
|
|
145
|
-
text String
|
|
146
|
-
|
|
147
|
-
// The SHA-256 hash of the footnote
|
|
148
|
-
sha256 String?
|
|
149
|
-
|
|
150
|
-
verseNumber Int?
|
|
151
|
-
verse ChapterVerse? @relation(fields: [translationId, bookId, chapterNumber, verseNumber], references: [translationId, bookId, chapterNumber, number])
|
|
152
|
-
|
|
153
|
-
@@id([translationId, bookId, chapterNumber, id])
|
|
154
|
-
}
|
|
1
|
+
datasource db {
|
|
2
|
+
provider = "sqlite"
|
|
3
|
+
url = "file:../../bible-api.dev.db"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
generator client {
|
|
7
|
+
provider = "prisma-client-js"
|
|
8
|
+
output = "./prisma-gen"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
model Translation {
|
|
12
|
+
id String @id
|
|
13
|
+
name String
|
|
14
|
+
website String
|
|
15
|
+
licenseUrl String
|
|
16
|
+
shortName String?
|
|
17
|
+
englishName String
|
|
18
|
+
language String
|
|
19
|
+
textDirection String
|
|
20
|
+
|
|
21
|
+
// The SHA-256 hash of the translation
|
|
22
|
+
// includes everything about the translation, including the books, chapters, verses, footnotes, etc.
|
|
23
|
+
sha256 String?
|
|
24
|
+
|
|
25
|
+
books Book[]
|
|
26
|
+
chapters Chapter[]
|
|
27
|
+
verses ChapterVerse[]
|
|
28
|
+
footnotes ChapterFootnote[]
|
|
29
|
+
audioUrls ChapterAudioUrl[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
model InputFile {
|
|
33
|
+
// The ID of the translation that the file is for
|
|
34
|
+
translationId String
|
|
35
|
+
|
|
36
|
+
// The name of the file
|
|
37
|
+
name String
|
|
38
|
+
|
|
39
|
+
format String
|
|
40
|
+
|
|
41
|
+
// The SHA-256 hash of the file
|
|
42
|
+
sha256 String
|
|
43
|
+
|
|
44
|
+
sizeInBytes Int
|
|
45
|
+
|
|
46
|
+
@@id([translationId, name])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
model Book {
|
|
50
|
+
id String
|
|
51
|
+
|
|
52
|
+
translationId String
|
|
53
|
+
translation Translation @relation(fields: [translationId], references: [id])
|
|
54
|
+
|
|
55
|
+
name String
|
|
56
|
+
commonName String
|
|
57
|
+
title String?
|
|
58
|
+
order Int
|
|
59
|
+
|
|
60
|
+
numberOfChapters Int
|
|
61
|
+
|
|
62
|
+
// The SHA-256 hash of the book
|
|
63
|
+
sha256 String?
|
|
64
|
+
|
|
65
|
+
chapters Chapter[]
|
|
66
|
+
verses ChapterVerse[]
|
|
67
|
+
footnotes ChapterFootnote[]
|
|
68
|
+
audioUrls ChapterAudioUrl[]
|
|
69
|
+
|
|
70
|
+
@@id([translationId, id])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
model Chapter {
|
|
74
|
+
number Int
|
|
75
|
+
|
|
76
|
+
bookId String
|
|
77
|
+
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
78
|
+
|
|
79
|
+
translationId String
|
|
80
|
+
translation Translation @relation(fields: [translationId], references: [id])
|
|
81
|
+
|
|
82
|
+
json String // The JSON of the chapter
|
|
83
|
+
|
|
84
|
+
// The SHA-256 hash of the chapter
|
|
85
|
+
sha256 String?
|
|
86
|
+
|
|
87
|
+
verses ChapterVerse[]
|
|
88
|
+
footnotes ChapterFootnote[]
|
|
89
|
+
audioUrls ChapterAudioUrl[]
|
|
90
|
+
|
|
91
|
+
@@id([translationId, bookId, number])
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
model ChapterAudioUrl {
|
|
95
|
+
number Int
|
|
96
|
+
bookId String
|
|
97
|
+
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
98
|
+
|
|
99
|
+
translationId String
|
|
100
|
+
translation Translation @relation(fields: [translationId], references: [id])
|
|
101
|
+
|
|
102
|
+
chapter Chapter @relation(fields: [translationId, bookId, number], references: [translationId, bookId, number])
|
|
103
|
+
|
|
104
|
+
reader String
|
|
105
|
+
url String
|
|
106
|
+
|
|
107
|
+
@@id([translationId, bookId, number, reader])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
model ChapterVerse {
|
|
111
|
+
number Int
|
|
112
|
+
|
|
113
|
+
chapterNumber Int
|
|
114
|
+
chapter Chapter @relation(fields: [translationId, bookId, chapterNumber], references: [translationId, bookId, number])
|
|
115
|
+
|
|
116
|
+
bookId String
|
|
117
|
+
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
118
|
+
|
|
119
|
+
translationId String
|
|
120
|
+
translation Translation @relation(fields: [translationId], references: [id])
|
|
121
|
+
|
|
122
|
+
text String // The text of the verse
|
|
123
|
+
contentJson String // The JSON of the verse content
|
|
124
|
+
|
|
125
|
+
// The SHA-256 hash of the verse
|
|
126
|
+
sha256 String?
|
|
127
|
+
|
|
128
|
+
footnotes ChapterFootnote[]
|
|
129
|
+
|
|
130
|
+
@@id([translationId, bookId, chapterNumber, number])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
model ChapterFootnote {
|
|
134
|
+
id Int
|
|
135
|
+
|
|
136
|
+
chapterNumber Int
|
|
137
|
+
chapter Chapter @relation(fields: [translationId, bookId, chapterNumber], references: [translationId, bookId, number])
|
|
138
|
+
|
|
139
|
+
bookId String
|
|
140
|
+
book Book @relation(fields: [translationId, bookId], references: [translationId, id])
|
|
141
|
+
|
|
142
|
+
translationId String
|
|
143
|
+
translation Translation @relation(fields: [translationId], references: [id])
|
|
144
|
+
|
|
145
|
+
text String
|
|
146
|
+
|
|
147
|
+
// The SHA-256 hash of the footnote
|
|
148
|
+
sha256 String?
|
|
149
|
+
|
|
150
|
+
verseNumber Int?
|
|
151
|
+
verse ChapterVerse? @relation(fields: [translationId, bookId, chapterNumber, verseNumber], references: [translationId, bookId, chapterNumber, number])
|
|
152
|
+
|
|
153
|
+
@@id([translationId, bookId, chapterNumber, id])
|
|
154
|
+
}
|
package/uploads.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { SerializedFile
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { SerializedFile } from './db';
|
|
2
|
+
import { Uploader } from './files';
|
|
3
|
+
import { DatasetOutput } from '@helloao/tools/generation/dataset';
|
|
4
|
+
import { PrismaClient } from './prisma-gen';
|
|
5
|
+
import { GenerateApiOptions } from '@helloao/tools/generation/api';
|
|
6
|
+
export interface UploadApiFromDatabaseOptions extends UploadApiOptions, GenerateApiOptions {
|
|
5
7
|
/**
|
|
6
8
|
* The number of files to upload in each batch.
|
|
7
9
|
*/
|
|
@@ -29,6 +31,14 @@ export interface UploadApiOptions {
|
|
|
29
31
|
* The AWS profile to use for uploading to S3.
|
|
30
32
|
*/
|
|
31
33
|
profile?: string;
|
|
34
|
+
/**
|
|
35
|
+
* The AWS access key ID to use for uploading to S3.
|
|
36
|
+
*/
|
|
37
|
+
accessKeyId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The AWS secret access key to use for uploading to S3.
|
|
40
|
+
*/
|
|
41
|
+
secretAccessKey?: string;
|
|
32
42
|
/**
|
|
33
43
|
* Whether to generate API files that use the common name instead of book IDs.
|
|
34
44
|
*/
|
|
@@ -55,7 +65,7 @@ export declare function uploadApiFilesFromDatabase(db: PrismaClient, dest: strin
|
|
|
55
65
|
* @param options The options to use for the upload.
|
|
56
66
|
* @param datasets The datasets to generate the API files from.
|
|
57
67
|
*/
|
|
58
|
-
export declare function serializeAndUploadDatasets(dest: string, datasets: AsyncIterable<DatasetOutput>, options?: UploadApiOptions): Promise<void>;
|
|
68
|
+
export declare function serializeAndUploadDatasets(dest: string, datasets: AsyncIterable<DatasetOutput>, options?: UploadApiOptions & GenerateApiOptions): Promise<void>;
|
|
59
69
|
/**
|
|
60
70
|
* Uploads the given serialized files to the specified destination.
|
|
61
71
|
* @param dest The destination to upload the API files to. Supported destinations are S3, zip files, and local directories.
|
package/uploads.js
CHANGED
|
@@ -34,7 +34,9 @@ async function uploadApiFilesFromDatabase(db, dest, options) {
|
|
|
34
34
|
if (options.pretty) {
|
|
35
35
|
console.log('Generating pretty-printed JSON files');
|
|
36
36
|
}
|
|
37
|
-
const pageSize = typeof options.batchSize === 'number'
|
|
37
|
+
const pageSize = typeof options.batchSize === 'number'
|
|
38
|
+
? options.batchSize
|
|
39
|
+
: parseInt(options.batchSize);
|
|
38
40
|
await serializeAndUploadDatasets(dest, (0, db_1.loadDatasets)(db, pageSize, options.translations), options);
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
@@ -66,92 +68,10 @@ async function serializeAndUploadDatasets(dest, datasets, options = {}) {
|
|
|
66
68
|
if (options.pretty) {
|
|
67
69
|
console.log('Generating pretty-printed JSON files');
|
|
68
70
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const url = dest;
|
|
74
|
-
const s3Url = (0, s3_1.parseS3Url)(url);
|
|
75
|
-
if (!s3Url) {
|
|
76
|
-
throw new Error(`Invalid S3 URL: ${url}`);
|
|
77
|
-
}
|
|
78
|
-
if (!s3Url.bucketName) {
|
|
79
|
-
throw new Error(`Invalid S3 URL: ${url}\nUnable to determine bucket name`);
|
|
80
|
-
}
|
|
81
|
-
uploader = new s3_1.S3Uploader(s3Url.bucketName, s3Url.objectKey, options.profile ?? null);
|
|
82
|
-
}
|
|
83
|
-
else if (dest.startsWith('console://')) {
|
|
84
|
-
console.log('Uploading to console');
|
|
85
|
-
uploader = {
|
|
86
|
-
idealBatchSize: 50,
|
|
87
|
-
async upload(file, _overwrite) {
|
|
88
|
-
console.log(file.path);
|
|
89
|
-
console.log(file.content);
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
else if ((0, path_1.extname)(dest) === '.zip') {
|
|
95
|
-
console.log('Writing to zip file:', dest);
|
|
96
|
-
uploader = new files_1.ZipUploader(dest);
|
|
97
|
-
}
|
|
98
|
-
else if (dest) {
|
|
99
|
-
console.log('Writing to local directory:', dest);
|
|
100
|
-
uploader = new files_1.FilesUploader(dest);
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
console.error('Unsupported destination:', dest);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
for await (let files of (0, db_1.serializeDatasets)(datasets, {
|
|
108
|
-
useCommonName: !!options.useCommonName,
|
|
109
|
-
generateAudioFiles: !!options.generateAudioFiles,
|
|
110
|
-
pretty: !!options.pretty,
|
|
111
|
-
})) {
|
|
112
|
-
const batchSize = uploader.idealBatchSize ?? files.length;
|
|
113
|
-
const totalBatches = Math.ceil(files.length / batchSize);
|
|
114
|
-
console.log('Uploading', files.length, 'total files');
|
|
115
|
-
console.log('Uploading in batches of', batchSize);
|
|
116
|
-
let offset = 0;
|
|
117
|
-
let batchNumber = 1;
|
|
118
|
-
let batch = files.slice(offset, offset + batchSize);
|
|
119
|
-
while (batch.length > 0) {
|
|
120
|
-
console.log('Uploading batch', batchNumber, 'of', totalBatches);
|
|
121
|
-
let writtenFiles = 0;
|
|
122
|
-
const promises = batch.map(async (file) => {
|
|
123
|
-
if (filePattern) {
|
|
124
|
-
if (!filePattern.test(file.path)) {
|
|
125
|
-
console.log('Skipping file:', file.path);
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const isAvailableTranslations = file.path.endsWith('available_translations.json');
|
|
130
|
-
const isCommonFile = !isAvailableTranslations;
|
|
131
|
-
if (await uploader.upload(file, overwrite || (overwriteCommonFiles && isCommonFile))) {
|
|
132
|
-
writtenFiles++;
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
console.warn('File already exists:', file.path);
|
|
136
|
-
console.warn('Skipping file');
|
|
137
|
-
}
|
|
138
|
-
if (file.content instanceof node_stream_1.Readable) {
|
|
139
|
-
file.content.destroy();
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
await Promise.all(promises);
|
|
143
|
-
console.log('Wrote', writtenFiles, 'files');
|
|
144
|
-
batchNumber++;
|
|
145
|
-
offset += batchSize;
|
|
146
|
-
batch = files.slice(offset, offset + batchSize);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
finally {
|
|
151
|
-
if (uploader && uploader.dispose) {
|
|
152
|
-
await uploader.dispose();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
71
|
+
const files = (0, db_1.serializeDatasets)(datasets, {
|
|
72
|
+
...options,
|
|
73
|
+
});
|
|
74
|
+
await uploadFiles(dest, options, files);
|
|
155
75
|
}
|
|
156
76
|
/**
|
|
157
77
|
* Uploads the given serialized files to the specified destination.
|
|
@@ -172,7 +92,7 @@ async function uploadFiles(dest, options, serializedFiles) {
|
|
|
172
92
|
if (!s3Url.bucketName) {
|
|
173
93
|
throw new Error(`Invalid S3 URL: ${url}\nUnable to determine bucket name`);
|
|
174
94
|
}
|
|
175
|
-
uploader = new s3_1.S3Uploader(s3Url.bucketName, s3Url.objectKey,
|
|
95
|
+
uploader = new s3_1.S3Uploader(s3Url.bucketName, s3Url.objectKey, (0, s3_1.defaultProviderForOptions)(options));
|
|
176
96
|
}
|
|
177
97
|
else if (dest.startsWith('console://')) {
|
|
178
98
|
console.log('Uploading to console');
|
|
@@ -182,7 +102,7 @@ async function uploadFiles(dest, options, serializedFiles) {
|
|
|
182
102
|
console.log(file.path);
|
|
183
103
|
console.log(file.content);
|
|
184
104
|
return true;
|
|
185
|
-
}
|
|
105
|
+
},
|
|
186
106
|
};
|
|
187
107
|
}
|
|
188
108
|
else if ((0, path_1.extname)(dest) === '.zip') {
|