@adminforth/markdown 1.3.0 → 1.4.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 +126 -36
  2. package/index.ts +169 -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,60 @@ 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 extractKeyFromUrl = (url) => url.replace(/^https:\/\/[^\/]+\/+/, '');
115
+ function getAttachmentMetas(markdown) {
116
+ var _a, _b, _c;
103
117
  if (!markdown) {
104
118
  return [];
105
119
  }
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"));
120
+ // Minimal image syntax: ![alt](src) or ![alt](src "title") or ![alt](src 'title')
121
+ // We only track https URLs and only those that look like S3/AWS public URLs.
122
+ const imageRegex = /!\[([^\]]*)\]\(\s*(https:\/\/[^\s)]+)\s*(?:\s+(?:"([^"]*)"|'([^']*)'))?\s*\)/g;
123
+ const byKey = new Map();
124
+ for (const match of markdown.matchAll(imageRegex)) {
125
+ const altRaw = (_a = match[1]) !== null && _a !== void 0 ? _a : '';
126
+ const srcRaw = match[2];
127
+ const titleRaw = (_c = ((_b = match[3]) !== null && _b !== void 0 ? _b : match[4])) !== null && _c !== void 0 ? _c : null;
128
+ const srcNoQuery = srcRaw.split('?')[0];
129
+ if (!srcNoQuery.includes('s3') && !srcNoQuery.includes('amazonaws')) {
130
+ continue;
131
+ }
132
+ const key = extractKeyFromUrl(srcNoQuery);
133
+ byKey.set(key, {
134
+ key,
135
+ alt: altRaw,
136
+ title: titleRaw,
137
+ });
138
+ }
139
+ return [...byKey.values()];
111
140
  }
112
- const createAttachmentRecords = (adminforth, options, recordId, s3Paths, adminUser) => __awaiter(this, void 0, void 0, function* () {
113
- const extractKey = (s3Paths) => s3Paths.replace(/^https:\/\/[^\/]+\/+/, '');
141
+ const createAttachmentRecords = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
142
+ if (!metas.length) {
143
+ return;
144
+ }
114
145
  process.env.HEAVY_DEBUG && console.log('📸 Creating attachment records', JSON.stringify(recordId));
115
146
  try {
116
- yield Promise.all(s3Paths.map((s3Path) => __awaiter(this, void 0, void 0, function* () {
117
- console.log('Processing path:', s3Path);
147
+ yield Promise.all(metas.map((meta) => __awaiter(this, void 0, void 0, function* () {
118
148
  try {
149
+ const recordToCreate = {
150
+ [options.attachments.attachmentFieldName]: meta.key,
151
+ [options.attachments.attachmentRecordIdFieldName]: recordId,
152
+ [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
153
+ };
154
+ if (options.attachments.attachmentTitleFieldName) {
155
+ recordToCreate[options.attachments.attachmentTitleFieldName] = meta.title;
156
+ }
157
+ if (options.attachments.attachmentAltFieldName) {
158
+ recordToCreate[options.attachments.attachmentAltFieldName] = meta.alt;
159
+ }
119
160
  yield adminforth.createResourceRecord({
120
161
  resource: this.attachmentResource,
121
- record: {
122
- [options.attachments.attachmentFieldName]: extractKey(s3Path),
123
- [options.attachments.attachmentRecordIdFieldName]: recordId,
124
- [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
125
- },
162
+ record: recordToCreate,
126
163
  adminUser,
127
164
  });
128
- console.log('Successfully created record for:', s3Path);
129
165
  }
130
166
  catch (err) {
131
- console.error('Error creating record for', s3Path, err);
167
+ console.error('Error creating record for', meta.key, err);
132
168
  }
133
169
  })));
134
170
  }
@@ -136,12 +172,16 @@ export default class MarkdownPlugin extends AdminForthPlugin {
136
172
  console.error('Error in Promise.all', err);
137
173
  }
138
174
  });
139
- const deleteAttachmentRecords = (adminforth, options, s3Paths, adminUser) => __awaiter(this, void 0, void 0, function* () {
140
- if (!s3Paths.length) {
175
+ const deleteAttachmentRecords = (adminforth, options, recordId, keys, adminUser) => __awaiter(this, void 0, void 0, function* () {
176
+ if (!keys.length) {
141
177
  return;
142
178
  }
143
179
  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));
180
+ const attachments = yield adminforth.resource(options.attachments.attachmentResource).list([
181
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
182
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId),
183
+ Filters.IN(options.attachments.attachmentFieldName, keys),
184
+ ]);
145
185
  yield Promise.all(attachments.map((a) => __awaiter(this, void 0, void 0, function* () {
146
186
  yield adminforth.deleteResourceRecord({
147
187
  resource: this.attachmentResource,
@@ -151,12 +191,58 @@ export default class MarkdownPlugin extends AdminForthPlugin {
151
191
  });
152
192
  })));
153
193
  });
194
+ const updateAttachmentRecordsMetadata = (adminforth, options, recordId, metas, adminUser) => __awaiter(this, void 0, void 0, function* () {
195
+ if (!metas.length) {
196
+ return;
197
+ }
198
+ if (!options.attachments.attachmentTitleFieldName && !options.attachments.attachmentAltFieldName) {
199
+ return;
200
+ }
201
+ const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
202
+ const metaByKey = new Map(metas.map(m => [m.key, m]));
203
+ const existingAparts = yield adminforth.resource(options.attachments.attachmentResource).list([
204
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
205
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
206
+ ]);
207
+ yield Promise.all(existingAparts.map((a) => __awaiter(this, void 0, void 0, function* () {
208
+ var _a, _b, _c, _d;
209
+ const key = a[options.attachments.attachmentFieldName];
210
+ const meta = metaByKey.get(key);
211
+ if (!meta) {
212
+ return;
213
+ }
214
+ const patch = {};
215
+ if (options.attachments.attachmentTitleFieldName) {
216
+ const field = options.attachments.attachmentTitleFieldName;
217
+ if (((_a = a[field]) !== null && _a !== void 0 ? _a : null) !== ((_b = meta.title) !== null && _b !== void 0 ? _b : null)) {
218
+ patch[field] = meta.title;
219
+ }
220
+ }
221
+ if (options.attachments.attachmentAltFieldName) {
222
+ const field = options.attachments.attachmentAltFieldName;
223
+ if (((_c = a[field]) !== null && _c !== void 0 ? _c : null) !== ((_d = meta.alt) !== null && _d !== void 0 ? _d : null)) {
224
+ patch[field] = meta.alt;
225
+ }
226
+ }
227
+ if (!Object.keys(patch).length) {
228
+ return;
229
+ }
230
+ yield adminforth.updateResourceRecord({
231
+ resource: this.attachmentResource,
232
+ recordId: a[attachmentPrimaryKeyField.name],
233
+ record: patch,
234
+ oldRecord: a,
235
+ adminUser,
236
+ });
237
+ })));
238
+ });
154
239
  (resourceConfig.hooks.create.afterSave).push((_a) => __awaiter(this, [_a], void 0, function* ({ record, adminUser }) {
155
240
  // find all s3Paths in the html
156
- const s3Paths = getAttachmentPathes(record[this.options.fieldName]);
157
- process.env.HEAVY_DEBUG && console.log('📸 Found s3Paths', s3Paths);
241
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
242
+ const keys = metas.map(m => m.key);
243
+ process.env.HEAVY_DEBUG && console.log('📸 Found attachment keys', keys);
158
244
  // create attachment records
159
- yield createAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], s3Paths, adminUser);
245
+ yield createAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], metas, adminUser);
160
246
  return { ok: true };
161
247
  }));
162
248
  // after edit we need to delete attachments that are not in the html anymore
@@ -172,18 +258,22 @@ export default class MarkdownPlugin extends AdminForthPlugin {
172
258
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, recordId),
173
259
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
174
260
  ]);
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);
261
+ const existingKeys = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
262
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
263
+ const newKeys = metas.map(m => m.key);
264
+ process.env.HEAVY_DEBUG && console.log('📸 Existing keys (from db)', existingKeys);
265
+ process.env.HEAVY_DEBUG && console.log('📸 Found new keys (from text)', newKeys);
266
+ const toDelete = existingKeys.filter(key => !newKeys.includes(key));
267
+ const toAdd = newKeys.filter(key => !existingKeys.includes(key));
268
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', toDelete);
269
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to add', toAdd);
270
+ const metasToAdd = metas.filter(m => toAdd.includes(m.key));
183
271
  yield Promise.all([
184
- deleteAttachmentRecords(adminforth, this.options, toDelete, adminUser),
185
- createAttachmentRecords(adminforth, this.options, recordId, toAdd, adminUser)
272
+ deleteAttachmentRecords(adminforth, this.options, recordId, toDelete, adminUser),
273
+ createAttachmentRecords(adminforth, this.options, recordId, metasToAdd, adminUser)
186
274
  ]);
275
+ // Keep alt/title in sync for existing attachments too
276
+ yield updateAttachmentRecordsMetadata(adminforth, this.options, recordId, metas, adminUser);
187
277
  return { ok: true };
188
278
  }));
189
279
  // after delete we need to delete all attachments
@@ -192,9 +282,9 @@ export default class MarkdownPlugin extends AdminForthPlugin {
192
282
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, record[editorRecordPkField.name]),
193
283
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
194
284
  ]);
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);
285
+ const existingKeys = existingAparts.map((a) => a[this.options.attachments.attachmentFieldName]);
286
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', existingKeys);
287
+ yield deleteAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], existingKeys, adminUser);
198
288
  return { ok: true };
199
289
  }));
200
290
  }
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,74 @@ 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 extractKeyFromUrl = (url: string) => url.replace(/^https:\/\/[^\/]+\/+/, '');
126
+
127
+ function getAttachmentMetas(markdown: string): AttachmentMeta[] {
110
128
  if (!markdown) {
111
129
  return [];
112
130
  }
113
131
 
114
- const s3PathRegex = /!\[.*?\]\((https:\/\/.*?\/.*?)(\?.*)?\)/g;
115
-
116
- const matches = [...markdown.matchAll(s3PathRegex)];
132
+ // Minimal image syntax: ![alt](src) or ![alt](src "title") or ![alt](src 'title')
133
+ // We only track https URLs and only those that look like S3/AWS public URLs.
134
+ const imageRegex = /!\[([^\]]*)\]\(\s*(https:\/\/[^\s)]+)\s*(?:\s+(?:"([^"]*)"|'([^']*)'))?\s*\)/g;
135
+
136
+ const byKey = new Map<string, AttachmentMeta>();
137
+ for (const match of markdown.matchAll(imageRegex)) {
138
+ const altRaw = match[1] ?? '';
139
+ const srcRaw = match[2];
140
+ const titleRaw = (match[3] ?? match[4]) ?? null;
141
+
142
+ const srcNoQuery = srcRaw.split('?')[0];
143
+ if (!srcNoQuery.includes('s3') && !srcNoQuery.includes('amazonaws')) {
144
+ continue;
145
+ }
117
146
 
118
- return matches
119
- .map(match => match[1])
120
- .filter(src => src.includes("s3") || src.includes("amazonaws"));
147
+ const key = extractKeyFromUrl(srcNoQuery);
148
+ byKey.set(key, {
149
+ key,
150
+ alt: altRaw,
151
+ title: titleRaw,
152
+ });
153
+ }
154
+ return [...byKey.values()];
121
155
  }
122
156
 
123
157
  const createAttachmentRecords = async (
124
- adminforth: IAdminForth, options: PluginOptions, recordId: any, s3Paths: string[], adminUser: AdminUser
158
+ adminforth: IAdminForth,
159
+ options: PluginOptions,
160
+ recordId: any,
161
+ metas: AttachmentMeta[],
162
+ adminUser: AdminUser
125
163
  ) => {
126
- const extractKey = (s3Paths: string) => s3Paths.replace(/^https:\/\/[^\/]+\/+/, '');
164
+ if (!metas.length) {
165
+ return;
166
+ }
127
167
  process.env.HEAVY_DEBUG && console.log('📸 Creating attachment records', JSON.stringify(recordId))
128
168
  try {
129
- await Promise.all(s3Paths.map(async (s3Path) => {
130
- console.log('Processing path:', s3Path);
169
+ await Promise.all(metas.map(async (meta) => {
131
170
  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);
171
+ const recordToCreate: any = {
172
+ [options.attachments.attachmentFieldName]: meta.key,
173
+ [options.attachments.attachmentRecordIdFieldName]: recordId,
174
+ [options.attachments.attachmentResourceIdFieldName]: resourceConfig.resourceId,
175
+ };
176
+
177
+ if (options.attachments.attachmentTitleFieldName) {
178
+ recordToCreate[options.attachments.attachmentTitleFieldName] = meta.title;
179
+ }
180
+ if (options.attachments.attachmentAltFieldName) {
181
+ recordToCreate[options.attachments.attachmentAltFieldName] = meta.alt;
182
+ }
183
+
184
+ await adminforth.createResourceRecord({
185
+ resource: this.attachmentResource,
186
+ record: recordToCreate,
187
+ adminUser,
188
+ });
144
189
  } catch (err) {
145
- console.error('Error creating record for', s3Path, err);
190
+ console.error('Error creating record for', meta.key, err);
146
191
  }
147
192
  }));
148
193
  } catch (err) {
@@ -151,35 +196,95 @@ export default class MarkdownPlugin extends AdminForthPlugin {
151
196
  }
152
197
 
153
198
  const deleteAttachmentRecords = async (
154
- adminforth: IAdminForth, options: PluginOptions, s3Paths: string[], adminUser: AdminUser
199
+ adminforth: IAdminForth,
200
+ options: PluginOptions,
201
+ recordId: any,
202
+ keys: string[],
203
+ adminUser: AdminUser
155
204
  ) => {
156
- if (!s3Paths.length) {
205
+ if (!keys.length) {
157
206
  return;
158
207
  }
159
208
  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
- );
209
+ const attachments = await adminforth.resource(options.attachments.attachmentResource).list([
210
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
211
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId),
212
+ Filters.IN(options.attachments.attachmentFieldName, keys),
213
+ ]);
214
+
163
215
  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
- )
216
+ await adminforth.deleteResourceRecord({
217
+ resource: this.attachmentResource,
218
+ recordId: a[attachmentPrimaryKeyField.name],
219
+ adminUser,
220
+ record: a,
221
+ })
172
222
  }))
173
223
  }
224
+
225
+ const updateAttachmentRecordsMetadata = async (
226
+ adminforth: IAdminForth,
227
+ options: PluginOptions,
228
+ recordId: any,
229
+ metas: AttachmentMeta[],
230
+ adminUser: AdminUser
231
+ ) => {
232
+ if (!metas.length) {
233
+ return;
234
+ }
235
+ if (!options.attachments.attachmentTitleFieldName && !options.attachments.attachmentAltFieldName) {
236
+ return;
237
+ }
238
+ const attachmentPrimaryKeyField = this.attachmentResource.columns.find(c => c.primaryKey);
239
+ const metaByKey = new Map(metas.map(m => [m.key, m] as const));
240
+
241
+ const existingAparts = await adminforth.resource(options.attachments.attachmentResource).list([
242
+ Filters.EQ(options.attachments.attachmentRecordIdFieldName, recordId),
243
+ Filters.EQ(options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
244
+ ]);
245
+
246
+ await Promise.all(existingAparts.map(async (a: any) => {
247
+ const key = a[options.attachments.attachmentFieldName];
248
+ const meta = metaByKey.get(key);
249
+ if (!meta) {
250
+ return;
251
+ }
252
+
253
+ const patch: any = {};
254
+ if (options.attachments.attachmentTitleFieldName) {
255
+ const field = options.attachments.attachmentTitleFieldName;
256
+ if ((a[field] ?? null) !== (meta.title ?? null)) {
257
+ patch[field] = meta.title;
258
+ }
259
+ }
260
+ if (options.attachments.attachmentAltFieldName) {
261
+ const field = options.attachments.attachmentAltFieldName;
262
+ if ((a[field] ?? null) !== (meta.alt ?? null)) {
263
+ patch[field] = meta.alt;
264
+ }
265
+ }
266
+ if (!Object.keys(patch).length) {
267
+ return;
268
+ }
269
+
270
+ await adminforth.updateResourceRecord({
271
+ resource: this.attachmentResource,
272
+ recordId: a[attachmentPrimaryKeyField.name],
273
+ record: patch,
274
+ oldRecord: a,
275
+ adminUser,
276
+ });
277
+ }));
278
+ }
174
279
 
175
280
  (resourceConfig.hooks.create.afterSave).push(async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {
176
281
  // 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);
282
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
283
+ const keys = metas.map(m => m.key);
284
+ process.env.HEAVY_DEBUG && console.log('📸 Found attachment keys', keys);
180
285
  // create attachment records
181
286
  await createAttachmentRecords(
182
- adminforth, this.options, record[editorRecordPkField.name], s3Paths, adminUser);
287
+ adminforth, this.options, record[editorRecordPkField.name], metas, adminUser);
183
288
 
184
289
  return { ok: true };
185
290
  });
@@ -198,19 +303,29 @@ export default class MarkdownPlugin extends AdminForthPlugin {
198
303
  Filters.EQ(this.options.attachments.attachmentRecordIdFieldName, recordId),
199
304
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
200
305
  ]);
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);
306
+ const existingKeys = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
307
+
308
+ const metas = getAttachmentMetas(record[this.options.fieldName]);
309
+ const newKeys = metas.map(m => m.key);
310
+
311
+ process.env.HEAVY_DEBUG && console.log('📸 Existing keys (from db)', existingKeys)
312
+ process.env.HEAVY_DEBUG && console.log('📸 Found new keys (from text)', newKeys);
313
+
314
+ const toDelete = existingKeys.filter(key => !newKeys.includes(key));
315
+ const toAdd = newKeys.filter(key => !existingKeys.includes(key));
316
+
317
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', toDelete)
318
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to add', toAdd);
319
+
320
+ const metasToAdd = metas.filter(m => toAdd.includes(m.key));
209
321
  await Promise.all([
210
- deleteAttachmentRecords(adminforth, this.options, toDelete, adminUser),
211
- createAttachmentRecords(adminforth, this.options, recordId, toAdd, adminUser)
322
+ deleteAttachmentRecords(adminforth, this.options, recordId, toDelete, adminUser),
323
+ createAttachmentRecords(adminforth, this.options, recordId, metasToAdd, adminUser)
212
324
  ]);
213
325
 
326
+ // Keep alt/title in sync for existing attachments too
327
+ await updateAttachmentRecordsMetadata(adminforth, this.options, recordId, metas, adminUser);
328
+
214
329
  return { ok: true };
215
330
 
216
331
  }
@@ -225,9 +340,9 @@ export default class MarkdownPlugin extends AdminForthPlugin {
225
340
  Filters.EQ(this.options.attachments.attachmentResourceIdFieldName, resourceConfig.resourceId)
226
341
  ]
227
342
  );
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);
343
+ const existingKeys = existingAparts.map((a: any) => a[this.options.attachments.attachmentFieldName]);
344
+ process.env.HEAVY_DEBUG && console.log('📸 Found keys to delete', existingKeys);
345
+ await deleteAttachmentRecords(adminforth, this.options, record[editorRecordPkField.name], existingKeys, adminUser);
231
346
 
232
347
  return { ok: true };
233
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/markdown",
3
- "version": "1.3.0",
3
+ "version": "1.4.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
  }