backend-manager 5.0.134 → 5.0.135

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.134",
3
+ "version": "5.0.135",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * POST /admin/post - Create blog post
3
3
  * Admin/blogger endpoint to create blog posts via GitHub
4
+ * Uses Git Trees API to commit all files (images + post) in a single commit
4
5
  */
5
6
  const moment = require('moment');
6
7
  const jetpack = require('fs-jetpack');
@@ -15,7 +16,6 @@ const IMAGE_PATH_SRC = `src/assets/images/blog/post-{id}/`;
15
16
  const IMAGE_REGEX = /(?:!\[(.*?)\]\((.*?)\))/img;
16
17
 
17
18
  module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
18
- const fetch = Manager.require('wonderful-fetch');
19
19
 
20
20
  // Require authentication
21
21
  if (!user.authenticated) {
@@ -92,30 +92,47 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
92
92
 
93
93
  assistant.log('main(): Creating post...', settings);
94
94
 
95
- // Extract all images
96
- const imageResult = await extractImages(assistant, octokit, settings).catch(e => e);
97
- if (imageResult instanceof Error) {
98
- return assistant.respond(imageResult.message, { code: 400 });
95
+ // Download all images and collect file data
96
+ const imageFiles = await downloadImages(assistant, settings).catch(e => e);
97
+ if (imageFiles instanceof Error) {
98
+ return assistant.respond(imageFiles.message, { code: 400 });
99
99
  }
100
100
 
101
101
  // Rewrite body to use @post/ prefix for extracted images
102
- for (const { originalUrl, localFilename } of imageResult) {
103
- settings.body = settings.body.split(originalUrl).join(`@post/${localFilename}`);
102
+ for (const file of imageFiles) {
103
+ if (file.originalUrl) {
104
+ settings.body = settings.body.split(file.originalUrl).join(`@post/${file.filename}`);
105
+ }
104
106
  }
105
107
 
106
- // Set defaults
108
+ // Generate post content from template
107
109
  const formattedContent = powertools.template(
108
110
  POST_TEMPLATE,
109
111
  formatClone(settings),
110
112
  );
111
113
 
112
- // Upload post
113
- const uploadResult = await uploadPost(assistant, octokit, settings, formattedContent).catch(e => e);
114
- if (uploadResult instanceof Error) {
115
- return assistant.respond(uploadResult.message, { code: 500 });
114
+ // Build post file entry
115
+ const postFilename = `${settings.path}/${settings.date}-${settings.url}.md`;
116
+ const allFiles = [
117
+ ...imageFiles.map(img => ({
118
+ path: img.githubPath,
119
+ content: img.base64,
120
+ encoding: 'base64',
121
+ })),
122
+ {
123
+ path: postFilename,
124
+ content: Buffer.from(formattedContent).toString('base64'),
125
+ encoding: 'base64',
126
+ },
127
+ ];
128
+
129
+ // Commit all files in a single commit
130
+ const commitResult = await commitAll(assistant, octokit, settings, allFiles).catch(e => e);
131
+ if (commitResult instanceof Error) {
132
+ return assistant.respond(commitResult.message, { code: 500 });
116
133
  }
117
134
 
118
- assistant.log('main(): uploadPost', uploadResult);
135
+ assistant.log('main(): commitAll', commitResult);
119
136
 
120
137
  // Track analytics
121
138
  analytics.event('admin/post', { action: 'create' });
@@ -123,9 +140,10 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
123
140
  return assistant.respond(settings);
124
141
  };
125
142
 
126
- // Helper: Extract and upload images
127
- async function extractImages(assistant, octokit, settings) {
128
- const urlMap = [];
143
+ // Helper: Download all images and return file data (no GitHub uploads)
144
+ async function downloadImages(assistant, settings) {
145
+ const files = [];
146
+ const assetsPath = powertools.template(IMAGE_PATH_SRC, settings);
129
147
 
130
148
  const matches = settings.body.matchAll(IMAGE_REGEX);
131
149
  const images = Array.from(matches).map(match => ({
@@ -141,10 +159,10 @@ async function extractImages(assistant, octokit, settings) {
141
159
  header: true,
142
160
  });
143
161
 
144
- assistant.log('extractImages(): images', images);
162
+ assistant.log('downloadImages(): images', images);
145
163
 
146
164
  if (!images.length) {
147
- return urlMap;
165
+ return files;
148
166
  }
149
167
 
150
168
  for (let index = 0; index < images.length; index++) {
@@ -153,38 +171,29 @@ async function extractImages(assistant, octokit, settings) {
153
171
  // Download image
154
172
  const download = await downloadImage(assistant, image.src, image.alt).catch(e => e);
155
173
 
156
- assistant.log('extractImages(): download', download);
174
+ assistant.log('downloadImages(): download', download);
157
175
 
158
176
  if (download instanceof Error) {
159
177
  if (image.header) {
160
178
  throw download;
161
179
  } else {
162
- assistant.warn('extractImages(): Skipping NON-HEADER image download due to error', download);
180
+ assistant.warn('downloadImages(): Skipping NON-HEADER image download due to error', download);
163
181
  continue;
164
182
  }
165
183
  }
166
184
 
167
- // Upload image
168
- const upload = await uploadImage(assistant, octokit, settings, download).catch(e => e);
185
+ // Read file content as base64
186
+ const base64 = jetpack.read(download.path, 'buffer').toString('base64');
169
187
 
170
- assistant.log('extractImages(): upload', upload);
171
-
172
- if (upload instanceof Error) {
173
- if (image.header) {
174
- throw upload;
175
- } else {
176
- assistant.warn('extractImages(): Skipping NON-HEADER image upload due to error', upload);
177
- continue;
178
- }
179
- }
180
-
181
- // Track successfully uploaded non-header images for body rewriting
182
- if (!image.header) {
183
- urlMap.push({ originalUrl: image.src, localFilename: download.filename });
184
- }
188
+ files.push({
189
+ githubPath: `${assetsPath}${download.filename}`,
190
+ filename: download.filename,
191
+ base64: base64,
192
+ originalUrl: image.header ? null : image.src,
193
+ });
185
194
  }
186
195
 
187
- return urlMap;
196
+ return files;
188
197
  }
189
198
 
190
199
  // Helper: Download image
@@ -211,79 +220,95 @@ async function downloadImage(assistant, src, alt) {
211
220
  return result;
212
221
  }
213
222
 
214
- // Helper: Upload image to GitHub
215
- async function uploadImage(assistant, octokit, settings, image) {
216
- const filepath = image.path;
217
- const filename = image.filename;
218
- const assetsPath = powertools.template(IMAGE_PATH_SRC, settings);
223
+ // Helper: Commit all files (images + post) in a single commit using Git Trees API
224
+ async function commitAll(assistant, octokit, settings, files) {
219
225
  const owner = settings.githubUser;
220
226
  const repo = settings.githubRepo;
221
227
 
222
- assistant.log('uploadImage(): image', image);
223
- assistant.log('uploadImage(): path', `${assetsPath}${filename}`);
228
+ assistant.log('commitAll(): Committing', files.length, 'files');
224
229
 
225
- // Get existing image
226
- const existing = await octokit.rest.repos.getContent({
230
+ // Get the latest commit SHA on the default branch
231
+ const refResult = await octokit.rest.git.getRef({
227
232
  owner: owner,
228
233
  repo: repo,
229
- path: `${assetsPath}${filename}`,
230
- }).catch(e => e);
234
+ ref: 'heads/master',
235
+ }).catch(() => {
236
+ // Try 'main' if 'master' fails
237
+ return octokit.rest.git.getRef({
238
+ owner: owner,
239
+ repo: repo,
240
+ ref: 'heads/main',
241
+ });
242
+ });
231
243
 
232
- assistant.log('uploadImage(): Existing', existing);
244
+ const latestCommitSha = refResult.data.object.sha;
245
+ const branch = refResult.data.ref;
233
246
 
234
- if (existing instanceof Error && existing?.status !== 404) {
235
- throw existing;
236
- }
247
+ assistant.log('commitAll(): Latest commit', latestCommitSha, 'on', branch);
237
248
 
238
- // Upload image
239
- const result = await octokit.rest.repos.createOrUpdateFileContents({
249
+ // Get the tree SHA of the latest commit
250
+ const commitResult = await octokit.rest.git.getCommit({
240
251
  owner: owner,
241
252
  repo: repo,
242
- path: `${assetsPath}${filename}`,
243
- sha: existing?.data?.sha || undefined,
244
- message: `📦 admin/post:upload-image ${filename}`,
245
- content: jetpack.read(filepath, 'buffer').toString('base64'),
253
+ commit_sha: latestCommitSha,
246
254
  });
247
255
 
248
- assistant.log('uploadImage(): Result', result);
256
+ const baseTreeSha = commitResult.data.tree.sha;
249
257
 
250
- return result;
251
- }
258
+ // Create blobs for each file
259
+ const treeItems = [];
252
260
 
253
- // Helper: Upload post to GitHub
254
- async function uploadPost(assistant, octokit, settings, content) {
255
- const filename = `${settings.path}/${settings.date}-${settings.url}.md`;
256
- const owner = settings.githubUser;
257
- const repo = settings.githubRepo;
261
+ for (const file of files) {
262
+ const blob = await octokit.rest.git.createBlob({
263
+ owner: owner,
264
+ repo: repo,
265
+ content: file.content,
266
+ encoding: file.encoding,
267
+ });
258
268
 
259
- assistant.log('uploadPost(): filename', filename);
269
+ assistant.log('commitAll(): Created blob for', file.path, blob.data.sha);
260
270
 
261
- // Get existing post
262
- const existing = await octokit.rest.repos.getContent({
271
+ treeItems.push({
272
+ path: file.path,
273
+ mode: '100644',
274
+ type: 'blob',
275
+ sha: blob.data.sha,
276
+ });
277
+ }
278
+
279
+ // Create a new tree with all files
280
+ const newTree = await octokit.rest.git.createTree({
263
281
  owner: owner,
264
282
  repo: repo,
265
- path: filename,
266
- }).catch(e => e);
283
+ base_tree: baseTreeSha,
284
+ tree: treeItems,
285
+ });
267
286
 
268
- assistant.log('uploadPost(): Existing', existing);
287
+ assistant.log('commitAll(): Created tree', newTree.data.sha);
269
288
 
270
- if (existing instanceof Error && existing?.status !== 404) {
271
- throw existing;
272
- }
289
+ // Create the commit
290
+ const postPath = files[files.length - 1].path;
291
+ const newCommit = await octokit.rest.git.createCommit({
292
+ owner: owner,
293
+ repo: repo,
294
+ message: `📦 admin/post:create ${postPath}`,
295
+ tree: newTree.data.sha,
296
+ parents: [latestCommitSha],
297
+ });
298
+
299
+ assistant.log('commitAll(): Created commit', newCommit.data.sha);
273
300
 
274
- // Upload post
275
- const result = await octokit.rest.repos.createOrUpdateFileContents({
301
+ // Update the branch ref to point to the new commit
302
+ const updateResult = await octokit.rest.git.updateRef({
276
303
  owner: owner,
277
304
  repo: repo,
278
- path: filename,
279
- sha: existing?.data?.sha || undefined,
280
- message: `📦 admin/post:upload-post ${filename}`,
281
- content: Buffer.from(content).toString('base64'),
305
+ ref: branch.replace('refs/', ''),
306
+ sha: newCommit.data.sha,
282
307
  });
283
308
 
284
- assistant.log('uploadPost(): Result', result);
309
+ assistant.log('commitAll(): Updated ref', updateResult.data.object.sha);
285
310
 
286
- return result;
311
+ return updateResult;
287
312
  }
288
313
 
289
314
  // Helper: Format clone for templating