@adminforth/markdown 1.3.0 → 1.5.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 (4) hide show
  1. package/dist/index.js +143 -36
  2. package/index.ts +186 -54
  3. package/package.json +1 -1
  4. package/types.ts +14 -0
package/dist/index.js CHANGED
@@ -54,6 +54,18 @@ export default class MarkdownPlugin extends AdminForthPlugin {
54
54
  if (!field) {
55
55
  throw new Error(`Field '${this.options.attachments.attachmentFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
56
56
  }
57
+ if (this.options.attachments.attachmentTitleFieldName) {
58
+ const titleField = yield resource.columns.find(c => c.name === this.options.attachments.attachmentTitleFieldName);
59
+ if (!titleField) {
60
+ throw new Error(`Field '${this.options.attachments.attachmentTitleFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
61
+ }
62
+ }
63
+ if (this.options.attachments.attachmentAltFieldName) {
64
+ const altField = yield resource.columns.find(c => c.name === this.options.attachments.attachmentAltFieldName);
65
+ if (!altField) {
66
+ throw new Error(`Field '${this.options.attachments.attachmentAltFieldName}' not found in resource '${this.options.attachments.attachmentResource}'`);
67
+ }
68
+ }
57
69
  const plugin = yield adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.options.attachments.attachmentResource &&
58
70
  p.pluginOptions.pathColumnName === this.options.attachments.attachmentFieldName);
59
71
  if (!plugin) {
@@ -99,36 +111,77 @@ export default class MarkdownPlugin extends AdminForthPlugin {
99
111
  };
100
112
  const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
101
113
  if (this.options.attachments) {
102
- function getAttachmentPathes(markdown) {
114
+ const stripQueryAndHash = (value) => value.split('#')[0].split('?')[0];
115
+ const extractKeyFromUrl = (url) => {
116
+ // Supports absolute https/http URLs and protocol-relative URLs.
117
+ // Returns the object key as a path without leading slashes.
118
+ try {
119
+ const normalized = url.startsWith('//') ? `https:${url}` : url;
120
+ const u = new URL(normalized);
121
+ return u.pathname.replace(/^\/+/, '');
122
+ }
123
+ catch (_a) {
124
+ // Fallback: strip scheme/host if it looks like a URL, otherwise treat as a path.
125
+ return stripQueryAndHash(url).replace(/^https?:\/\/[^\/]+\/+/, '').replace(/^\/+/, '');
126
+ }
127
+ };
128
+ function getAttachmentMetas(markdown) {
129
+ var _a, _b, _c;
103
130
  if (!markdown) {
104
131
  return [];
105
132
  }
106
- const s3PathRegex = /!\[.*?\]\((https:\/\/.*?\/.*?)(\?.*)?\)/g;
107
- const matches = [...markdown.matchAll(s3PathRegex)];
108
- return matches
109
- .map(match => match[1])
110
- .filter(src => src.includes("s3") || src.includes("amazonaws"));
133
+ // Minimal image syntax: ![alt](src) or ![alt](src "title") or ![alt](src 'title')
134
+ // We track external (http/https) and relative sources, but skip data: URLs.
135
+ const imageRegex = /!\[([^\]]*)\]\(\s*([^\s)]+)\s*(?:\s+(?:\"([^\"]*)\"|'([^']*)'))?\s*\)/g;
136
+ const byKey = new Map();
137
+ for (const match of markdown.matchAll(imageRegex)) {
138
+ const altRaw = (_a = match[1]) !== null && _a !== void 0 ? _a : '';
139
+ const srcRaw = match[2];
140
+ const titleRaw = (_c = ((_b = match[3]) !== null && _b !== void 0 ? _b : match[4])) !== null && _c !== void 0 ? _c : null;
141
+ const srcTrimmed = srcRaw.trim().replace(/^<|>$/g, '');
142
+ if (!srcTrimmed || srcTrimmed.startsWith('data:')) {
143
+ continue;
144
+ }
145
+ const srcNoQuery = stripQueryAndHash(srcTrimmed);
146
+ const key = extractKeyFromUrl(srcNoQuery);
147
+ if (!key) {
148
+ continue;
149
+ }
150
+ byKey.set(key, {
151
+ key,
152
+ alt: altRaw,
153
+ title: titleRaw,
154
+ });
155
+ }
156
+ return [...byKey.values()];
111
157
  }
112
- const createAttachmentRecords = (adminforth, options, recordId, s3Paths, adminUser) => __awaiter(this, void 0, void 0, function* () {
113
- const extractKey = (s3Paths) => s3Paths.replace(/^https:\/\/[^\/]+\/+/, '');
158
+ const createAttachmentRecords = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
159
+ if (!metas.length) {
160
+ return;
161
+ }
114
162
  process.env.HEAVY_DEBUG && console.log('📸 Creating attachment records', JSON.stringify(recordId));
115
163
  try {
116
- yield Promise.all(s3Paths.map((s3Path) => __awaiter(this, void 0, void 0, function* () {
117
- console.log('Processing path:', s3Path);
164
+ yield Promise.all(metas.map((meta) => __awaiter(this, void 0, void 0, function* () {
118
165
  try {
166
+ const recordToCreate = {
167
+ [options.attachments.attachmentFieldName]: meta.key,
168
+ [options.attachments.attachmentRecordIdFieldName]: recordId,
169
+ [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
170
+ };
171
+ if (options.attachments.attachmentTitleFieldName) {
172
+ recordToCreate[options.attachments.attachmentTitleFieldName] = meta.title;
173
+ }
174
+ if (options.attachments.attachmentAltFieldName) {
175
+ recordToCreate[options.attachments.attachmentAltFieldName] = meta.alt;
176
+ }
119
177
  yield adminforth.createResourceRecord({
120
178
  resource: this.attachmentResource,
121
- record: {
122
- [options.attachments.attachmentFieldName]: extractKey(s3Path),
123
- [options.attachments.attachmentRecordIdFieldName]: recordId,
124
- [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
125
- },
179
+ record: recordToCreate,
126
180
  adminUser,
127
181
  });
128
- console.log('Successfully created record for:', s3Path);
129
182
  }
130
183
  catch (err) {
131
- console.error('Error creating record for', s3Path, err);
184
+ console.error('Error creating record for', meta.key, err);
132
185
  }
133
186
  })));
134
187
  }
@@ -136,12 +189,16 @@ export default class MarkdownPlugin extends AdminForthPlugin {
136
189
  console.error('Error in Promise.all', err);
137
190
  }
138
191
  });
139
- const deleteAttachmentRecords = (adminforth, options, s3Paths, adminUser) => __awaiter(this, void 0, void 0, function* () {
140
- if (!s3Paths.length) {
192
+ const deleteAttachmentRecords = (adminforth, options, recordId, keys, adminUser) => __awaiter(this, void 0, void 0, function* () {
193
+ if (!keys.length) {
141
194
  return;
142
195
  }
143
196
  const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
144
- const attachments = yield adminforth.resource(options.attachments.attachmentResource).list(Filters.IN(options.attachments.attachmentFieldName, s3Paths));
197
+ const attachments = yield adminforth.resource(options.attachments.attachmentResource).list([
198
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
199
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId),
200
+ Filters.IN(options.attachments.attachmentFieldName, keys),
201
+ ]);
145
202
  yield Promise.all(attachments.map((a) => __awaiter(this, void 0, void 0, function* () {
146
203
  yield adminforth.deleteResourceRecord({
147
204
  resource: this.attachmentResource,
@@ -151,12 +208,58 @@ export default class MarkdownPlugin extends AdminForthPlugin {
151
208
  });
152
209
  })));
153
210
  });
211
+ const updateAttachmentRecordsMetadata = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
212
+ if (!metas.length) {
213
+ return;
214
+ }
215
+ if (!options.attachments.attachmentTitleFieldName && !options.attachments.attachmentAltFieldName) {
216
+ return;
217
+ }
218
+ const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
219
+ const metaByKey = new Map(metas.map(m => [m.key, m]));
220
+ const existingAparts = yield adminforth.resource(options.attachments.attachmentResource).list([
221
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
222
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
223
+ ]);
224
+ yield Promise.all(existingAparts.map((a) => __awaiter(this, void 0, void 0, function* () {
225
+ var _a, _b, _c, _d;
226
+ const key = a[options.attachments.attachmentFieldName];
227
+ const meta = metaByKey.get(key);
228
+ if (!meta) {
229
+ return;
230
+ }
231
+ const patch = {};
232
+ if (options.attachments.attachmentTitleFieldName) {
233
+ const field = options.attachments.attachmentTitleFieldName;
234
+ if (((_a = a[field]) !== null && _a !== void 0 ? _a : null) !== ((_b = meta.title) !== null && _b !== void 0 ? _b : null)) {
235
+ patch[field] = meta.title;
236
+ }
237
+ }
238
+ if (options.attachments.attachmentAltFieldName) {
239
+ const field = options.attachments.attachmentAltFieldName;
240
+ if (((_c = a[field]) !== null && _c !== void 0 ? _c : null) !== ((_d = meta.alt) !== null && _d !== void 0 ? _d : null)) {
241
+ patch[field] = meta.alt;
242
+ }
243
+ }
244
+ if (!Object.keys(patch).length) {
245
+ return;
246
+ }
247
+ yield adminforth.updateResourceRecord({
248
+ resource: this.attachmentResource,
249
+ recordId: a[attachmentPrimaryKeyField.name],
250
+ record: patch,
251
+ oldRecord: a,
252
+ adminUser,
253
+ });
254
+ })));
255
+ });
154
256
  (resourceConfig.hooks.create.afterSave).push((_a) => __awaiter(this, [_a], void 0, function* ({ record, adminUser }) {
155
257
  // find all s3Paths in the html
156
- const s3Paths = getAttachmentPathes(record[this.options.fieldName]);
157
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths', s3Paths);
258
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
259
+ const keys = metas.map(m => m.key);
260
+ process.env.HEAVY_DEBUG && console.log('📸 Found attachment keys', keys);
158
261
  // create attachment records
159
- yield createAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], s3Paths, adminUser);
262
+ yield createAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], metas, adminUser);
160
263
  return { ok: true };
161
264
  }));
162
265
  // after edit we need to delete attachments that are not in the html anymore
@@ -172,18 +275,22 @@ export default class MarkdownPlugin extends AdminForthPlugin {
172
275
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, recordId),
173
276
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
174
277
  ]);
175
- const existingS3Paths = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
176
- const newS3Paths = getAttachmentPathes(record[this.options.fieldName]);
177
- process.env.HEAVY_DEBUG && console.log('📸 Existing s3Paths (from db)', existingS3Paths);
178
- process.env.HEAVY_DEBUG && console.log('📸 Found new s3Paths (from text)', newS3Paths);
179
- const toDelete = existingS3Paths.filter(s3Path => !newS3Paths.includes(s3Path));
180
- const toAdd = newS3Paths.filter(s3Path => !existingS3Paths.includes(s3Path));
181
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', toDelete);
182
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to add', toAdd);
278
+ const existingKeys = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
279
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
280
+ const newKeys = metas.map(m => m.key);
281
+ process.env.HEAVY_DEBUG && console.log('📸 Existing keys (from db)', existingKeys);
282
+ process.env.HEAVY_DEBUG && console.log('📸 Found new keys (from text)', newKeys);
283
+ const toDelete = existingKeys.filter(key => !newKeys.includes(key));
284
+ const toAdd = newKeys.filter(key => !existingKeys.includes(key));
285
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', toDelete);
286
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to add', toAdd);
287
+ const metasToAdd = metas.filter(m => toAdd.includes(m.key));
183
288
  yield Promise.all([
184
- deleteAttachmentRecords(adminforth, this.options, toDelete, adminUser),
185
- createAttachmentRecords(adminforth, this.options, recordId, toAdd, adminUser)
289
+ deleteAttachmentRecords(adminforth, this.options, recordId, toDelete, adminUser),
290
+ createAttachmentRecords(adminforth, this.options, recordId, metasToAdd, adminUser)
186
291
  ]);
292
+ // Keep alt/title in sync for existing attachments too
293
+ yield updateAttachmentRecordsMetadata(adminforth, this.options, recordId, metas, adminUser);
187
294
  return { ok: true };
188
295
  }));
189
296
  // after delete we need to delete all attachments
@@ -192,9 +299,9 @@ export default class MarkdownPlugin extends AdminForthPlugin {
192
299
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, record[editorRecordPkField.name]),
193
300
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
194
301
  ]);
195
- const existingS3Paths = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
196
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', existingS3Paths);
197
- yield deleteAttachmentRecords(adminforth, this.options, existingS3Paths, adminUser);
302
+ const existingKeys = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
303
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', existingKeys);
304
+ yield deleteAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], existingKeys, adminUser);
198
305
  return { ok: true };
199
306
  }));
200
307
  }
package/index.ts CHANGED
@@ -52,6 +52,20 @@ export default class MarkdownPlugin extends AdminForthPlugin {
52
52
  if (!field) {
53
53
  throw new Error(`Field '${this.options.attachments!.attachmentFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
54
54
  }
55
+
56
+ if (this.options.attachments.attachmentTitleFieldName) {
57
+ const titleField = await resource.columns.find(c => c.name === this.options.attachments!.attachmentTitleFieldName);
58
+ if (!titleField) {
59
+ throw new Error(`Field '${this.options.attachments!.attachmentTitleFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
60
+ }
61
+ }
62
+
63
+ if (this.options.attachments.attachmentAltFieldName) {
64
+ const altField = await resource.columns.find(c => c.name === this.options.attachments!.attachmentAltFieldName);
65
+ if (!altField) {
66
+ throw new Error(`Field '${this.options.attachments!.attachmentAltFieldName}' not found in resource '${this.options.attachments!.attachmentResource}'`);
67
+ }
68
+ }
55
69
 
56
70
  const plugin = await adminforth.activatedPlugins.find(p =>
57
71
  p.resourceConfig!.resourceId === this.options.attachments!.attachmentResource &&
@@ -106,43 +120,91 @@ export default class MarkdownPlugin extends AdminForthPlugin {
106
120
  const editorRecordPkField = resourceConfig.columns.find(c => c.primaryKey);
107
121
  if (this.options.attachments) {
108
122
 
109
- function getAttachmentPathes(markdown: string): string[] {
123
+ type AttachmentMeta = { key: string; alt: string | null; title: string | null };
124
+
125
+ const stripQueryAndHash = (value: string) => value.split('#')[0].split('?')[0];
126
+
127
+ const extractKeyFromUrl = (url: string) => {
128
+ // Supports absolute https/http URLs and protocol-relative URLs.
129
+ // Returns the object key as a path without leading slashes.
130
+ try {
131
+ const normalized = url.startsWith('//') ? `https:${url}` : url;
132
+ const u = new URL(normalized);
133
+ return u.pathname.replace(/^\/+/, '');
134
+ } catch {
135
+ // Fallback: strip scheme/host if it looks like a URL, otherwise treat as a path.
136
+ return stripQueryAndHash(url).replace(/^https?:\/\/[^\/]+\/+/, '').replace(/^\/+/, '');
137
+ }
138
+ };
139
+
140
+ function getAttachmentMetas(markdown: string): AttachmentMeta[] {
110
141
  if (!markdown) {
111
142
  return [];
112
143
  }
113
144
 
114
- const s3PathRegex = /!\[.*?\]\((https:\/\/.*?\/.*?)(\?.*)?\)/g;
115
-
116
- const matches = [...markdown.matchAll(s3PathRegex)];
145
+ // Minimal image syntax: ![alt](src) or ![alt](src "title") or ![alt](src 'title')
146
+ // We track external (http/https) and relative sources, but skip data: URLs.
147
+ const imageRegex = /!\[([^\]]*)\]\(\s*([^\s)]+)\s*(?:\s+(?:\"([^\"]*)\"|'([^']*)'))?\s*\)/g;
117
148
 
118
- return matches
119
- .map(match => match[1])
120
- .filter(src => src.includes("s3") || src.includes("amazonaws"));
149
+ const byKey = new Map<string, AttachmentMeta>();
150
+ for (const match of markdown.matchAll(imageRegex)) {
151
+ const altRaw = match[1] ?? '';
152
+ const srcRaw = match[2];
153
+ const titleRaw = (match[3] ?? match[4]) ?? null;
154
+
155
+ const srcTrimmed = srcRaw.trim().replace(/^<|>$/g, '');
156
+ if (!srcTrimmed || srcTrimmed.startsWith('data:')) {
157
+ continue;
158
+ }
159
+
160
+ const srcNoQuery = stripQueryAndHash(srcTrimmed);
161
+ const key = extractKeyFromUrl(srcNoQuery);
162
+ if (!key) {
163
+ continue;
164
+ }
165
+ byKey.set(key, {
166
+ key,
167
+ alt: altRaw,
168
+ title: titleRaw,
169
+ });
170
+ }
171
+ return [...byKey.values()];
121
172
  }
122
173
 
123
174
  const createAttachmentRecords = async (
124
- adminforth: IAdminForth, options: PluginOptions, recordId: any, s3Paths: string[], adminUser: AdminUser
175
+ adminforth: IAdminForth,
176
+ options: PluginOptions,
177
+ recordId: any,
178
+ metas: AttachmentMeta[],
179
+ adminUser: AdminUser
125
180
  ) => {
126
- const extractKey = (s3Paths: string) => s3Paths.replace(/^https:\/\/[^\/]+\/+/, '');
181
+ if (!metas.length) {
182
+ return;
183
+ }
127
184
  process.env.HEAVY_DEBUG && console.log('📸 Creating attachment records', JSON.stringify(recordId))
128
185
  try {
129
- await Promise.all(s3Paths.map(async (s3Path) => {
130
- console.log('Processing path:', s3Path);
186
+ await Promise.all(metas.map(async (meta) => {
131
187
  try {
132
- await adminforth.createResourceRecord(
133
- {
134
- resource: this.attachmentResource,
135
- record: {
136
- [options.attachments.attachmentFieldName]: extractKey(s3Path),
137
- [options.attachments.attachmentRecordIdFieldName]: recordId,
138
- [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
139
- },
140
- adminUser,
141
- }
142
- );
143
- console.log('Successfully created record for:', s3Path);
188
+ const recordToCreate: any = {
189
+ [options.attachments.attachmentFieldName]: meta.key,
190
+ [options.attachments.attachmentRecordIdFieldName]: recordId,
191
+ [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
192
+ };
193
+
194
+ if (options.attachments.attachmentTitleFieldName) {
195
+ recordToCreate[options.attachments.attachmentTitleFieldName] = meta.title;
196
+ }
197
+ if (options.attachments.attachmentAltFieldName) {
198
+ recordToCreate[options.attachments.attachmentAltFieldName] = meta.alt;
199
+ }
200
+
201
+ await adminforth.createResourceRecord({
202
+ resource: this.attachmentResource,
203
+ record: recordToCreate,
204
+ adminUser,
205
+ });
144
206
  } catch (err) {
145
- console.error('Error creating record for', s3Path, err);
207
+ console.error('Error creating record for', meta.key, err);
146
208
  }
147
209
  }));
148
210
  } catch (err) {
@@ -151,35 +213,95 @@ export default class MarkdownPlugin extends AdminForthPlugin {
151
213
  }
152
214
 
153
215
  const deleteAttachmentRecords = async (
154
- adminforth: IAdminForth, options: PluginOptions, s3Paths: string[], adminUser: AdminUser
216
+ adminforth: IAdminForth,
217
+ options: PluginOptions,
218
+ recordId: any,
219
+ keys: string[],
220
+ adminUser: AdminUser
155
221
  ) => {
156
- if (!s3Paths.length) {
222
+ if (!keys.length) {
157
223
  return;
158
224
  }
159
225
  const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
160
- const attachments = await adminforth.resource(options.attachments.attachmentResource).list(
161
- Filters.IN(options.attachments.attachmentFieldName, s3Paths)
162
- );
226
+ const attachments = await adminforth.resource(options.attachments.attachmentResource).list([
227
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
228
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId),
229
+ Filters.IN(options.attachments.attachmentFieldName, keys),
230
+ ]);
231
+
163
232
  await Promise.all(attachments.map(async (a: any) => {
164
- await adminforth.deleteResourceRecord(
165
- {
166
- resource: this.attachmentResource,
167
- recordId: a[attachmentPrimaryKeyField.name],
168
- adminUser,
169
- record: a,
170
- }
171
- )
233
+ await adminforth.deleteResourceRecord({
234
+ resource: this.attachmentResource,
235
+ recordId: a[attachmentPrimaryKeyField.name],
236
+ adminUser,
237
+ record: a,
238
+ })
172
239
  }))
173
240
  }
241
+
242
+ const updateAttachmentRecordsMetadata = async (
243
+ adminforth: IAdminForth,
244
+ options: PluginOptions,
245
+ recordId: any,
246
+ metas: AttachmentMeta[],
247
+ adminUser: AdminUser
248
+ ) => {
249
+ if (!metas.length) {
250
+ return;
251
+ }
252
+ if (!options.attachments.attachmentTitleFieldName && !options.attachments.attachmentAltFieldName) {
253
+ return;
254
+ }
255
+ const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
256
+ const metaByKey = new Map(metas.map(m => [m.key, m] as const));
257
+
258
+ const existingAparts = await adminforth.resource(options.attachments.attachmentResource).list([
259
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
260
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
261
+ ]);
262
+
263
+ await Promise.all(existingAparts.map(async (a: any) => {
264
+ const key = a[options.attachments.attachmentFieldName];
265
+ const meta = metaByKey.get(key);
266
+ if (!meta) {
267
+ return;
268
+ }
269
+
270
+ const patch: any = {};
271
+ if (options.attachments.attachmentTitleFieldName) {
272
+ const field = options.attachments.attachmentTitleFieldName;
273
+ if ((a[field] ?? null) !== (meta.title ?? null)) {
274
+ patch[field] = meta.title;
275
+ }
276
+ }
277
+ if (options.attachments.attachmentAltFieldName) {
278
+ const field = options.attachments.attachmentAltFieldName;
279
+ if ((a[field] ?? null) !== (meta.alt ?? null)) {
280
+ patch[field] = meta.alt;
281
+ }
282
+ }
283
+ if (!Object.keys(patch).length) {
284
+ return;
285
+ }
286
+
287
+ await adminforth.updateResourceRecord({
288
+ resource: this.attachmentResource,
289
+ recordId: a[attachmentPrimaryKeyField.name],
290
+ record: patch,
291
+ oldRecord: a,
292
+ adminUser,
293
+ });
294
+ }));
295
+ }
174
296
 
175
297
  (resourceConfig.hooks.create.afterSave).push(async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {
176
298
  // find all s3Paths in the html
177
- const s3Paths = getAttachmentPathes(record[this.options.fieldName])
178
-
179
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths', s3Paths);
299
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
300
+ const keys = metas.map(m => m.key);
301
+ process.env.HEAVY_DEBUG && console.log('📸 Found attachment keys', keys);
180
302
  // create attachment records
181
303
  await createAttachmentRecords(
182
- adminforth, this.options, record[editorRecordPkField.name], s3Paths, adminUser);
304
+ adminforth, this.options, record[editorRecordPkField.name], metas, adminUser);
183
305
 
184
306
  return { ok: true };
185
307
  });
@@ -198,19 +320,29 @@ export default class MarkdownPlugin extends AdminForthPlugin {
198
320
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, recordId),
199
321
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
200
322
  ]);
201
- const existingS3Paths = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
202
- const newS3Paths = getAttachmentPathes(record[this.options.fieldName]);
203
- process.env.HEAVY_DEBUG && console.log('📸 Existing s3Paths (from db)', existingS3Paths)
204
- process.env.HEAVY_DEBUG && console.log('📸 Found new s3Paths (from text)', newS3Paths);
205
- const toDelete = existingS3Paths.filter(s3Path => !newS3Paths.includes(s3Path));
206
- const toAdd = newS3Paths.filter(s3Path => !existingS3Paths.includes(s3Path));
207
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', toDelete)
208
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to add', toAdd);
323
+ const existingKeys = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
324
+
325
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
326
+ const newKeys = metas.map(m => m.key);
327
+
328
+ process.env.HEAVY_DEBUG && console.log('📸 Existing keys (from db)', existingKeys)
329
+ process.env.HEAVY_DEBUG && console.log('📸 Found new keys (from text)', newKeys);
330
+
331
+ const toDelete = existingKeys.filter(key => !newKeys.includes(key));
332
+ const toAdd = newKeys.filter(key => !existingKeys.includes(key));
333
+
334
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', toDelete)
335
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to add', toAdd);
336
+
337
+ const metasToAdd = metas.filter(m => toAdd.includes(m.key));
209
338
  await Promise.all([
210
- deleteAttachmentRecords(adminforth, this.options, toDelete, adminUser),
211
- createAttachmentRecords(adminforth, this.options, recordId, toAdd, adminUser)
339
+ deleteAttachmentRecords(adminforth, this.options, recordId, toDelete, adminUser),
340
+ createAttachmentRecords(adminforth, this.options, recordId, metasToAdd, adminUser)
212
341
  ]);
213
342
 
343
+ // Keep alt/title in sync for existing attachments too
344
+ await updateAttachmentRecordsMetadata(adminforth, this.options, recordId, metas, adminUser);
345
+
214
346
  return { ok: true };
215
347
 
216
348
  }
@@ -225,9 +357,9 @@ export default class MarkdownPlugin extends AdminForthPlugin {
225
357
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
226
358
  ]
227
359
  );
228
- const existingS3Paths = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
229
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths to delete', existingS3Paths);
230
- await deleteAttachmentRecords(adminforth, this.options, existingS3Paths, adminUser);
360
+ const existingKeys = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
361
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', existingKeys);
362
+ await deleteAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], existingKeys, adminUser);
231
363
 
232
364
  return { ok: true };
233
365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/markdown",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Markdown plugin for adminforth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/types.ts CHANGED
@@ -38,5 +38,19 @@ export interface PluginOptions {
38
38
  * Why we force to store and ask for resource id if we already have record id? Because in amny use cases attachments resource is shared between multiple resources, and record id might be not be unique across resources, but resource id + record id will be always unique.
39
39
  */
40
40
  attachmentResourceIdFieldName: string; // e.g. 'apartment_resource_id',
41
+
42
+ /**
43
+ * Optional: field name in attachment resource where title of image will be stored.
44
+ * When in markdown title of image is mentioned e.g. ![alt](image.jpg "title"), it will be parsed and stored in attachment resource.
45
+ * If you will update title in markdown, it will be updated in attachment resource as well.
46
+ */
47
+ attachmentTitleFieldName?: string; // e.g. 'title',
48
+
49
+ /**
50
+ * Optional: field name in attachment resource where alt of image will be stored.
51
+ * When in markdown alt of image is mentioned e.g. ![alt](image.jpg), it will be parsed and stored in attachment resource.
52
+ * If you will update alt in markdown, it will be updated in attachment resource as well.
53
+ */
54
+ attachmentAltFieldName?: string; // e.g. 'alt',
41
55
  },
42
56
  }