@jant/core 0.3.46 → 0.3.48

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 (110) hide show
  1. package/bin/commands/db/execute-file.js +12 -4
  2. package/bin/commands/db/rehearse.js +2 -2
  3. package/bin/commands/export.js +12 -4
  4. package/bin/commands/import-site.js +60 -267
  5. package/bin/commands/migrate.js +36 -69
  6. package/bin/commands/reset-password.js +10 -4
  7. package/bin/commands/site/export.js +59 -248
  8. package/bin/commands/site/snapshot/export.js +58 -45
  9. package/bin/commands/site/snapshot/import.js +104 -52
  10. package/bin/lib/node-env.js +100 -0
  11. package/bin/lib/runtime-target.js +64 -0
  12. package/bin/lib/site-snapshot.js +185 -54
  13. package/bin/lib/sql-export.js +19 -2
  14. package/dist/app-DU7dpJID.js +6 -0
  15. package/dist/{app-DB-P66E5.js → app-DdnIoX7y.js} +333 -191
  16. package/dist/client/.vite/manifest.json +2 -2
  17. package/dist/client/_assets/client-BoUn7xBo.css +2 -0
  18. package/dist/client/_assets/{client-auth-BLCUje4M.js → client-auth-Ce5WEAVS.js} +102 -49
  19. package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
  20. package/dist/github-sync-C593r22F.js +4 -0
  21. package/dist/github-sync-bL1hnx3Q.js +428 -0
  22. package/dist/index.js +3 -2
  23. package/dist/node.js +5 -4
  24. package/package.json +3 -2
  25. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  26. package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
  27. package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
  28. package/src/client/components/jant-settings-general.ts +164 -22
  29. package/src/client/components/settings-types.ts +4 -6
  30. package/src/client-auth.ts +1 -1
  31. package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
  32. package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
  33. package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
  34. package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
  35. package/src/db/migrations/meta/0021_snapshot.json +2121 -0
  36. package/src/db/migrations/meta/_journal.json +7 -0
  37. package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
  38. package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
  39. package/src/db/migrations/pg/meta/_journal.json +7 -0
  40. package/src/db/pg/schema.ts +21 -26
  41. package/src/db/rehearsal-fixtures/demo-current.json +1 -1
  42. package/src/db/schema.ts +16 -20
  43. package/src/i18n/__tests__/middleware.test.ts +43 -1
  44. package/src/i18n/coverage.generated.ts +17 -0
  45. package/src/i18n/i18n.ts +18 -2
  46. package/src/i18n/index.ts +3 -0
  47. package/src/i18n/locales/settings/en.po +16 -11
  48. package/src/i18n/locales/settings/en.ts +1 -1
  49. package/src/i18n/locales/settings/zh-Hans.po +17 -12
  50. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  51. package/src/i18n/locales/settings/zh-Hant.po +16 -11
  52. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  53. package/src/i18n/locales.ts +84 -2
  54. package/src/i18n/middleware.ts +25 -16
  55. package/src/i18n/supported-locales.ts +153 -0
  56. package/src/lib/__tests__/csp-builder.test.ts +19 -2
  57. package/src/lib/__tests__/feed.test.ts +242 -1
  58. package/src/lib/__tests__/post-meta.test.ts +0 -1
  59. package/src/lib/__tests__/view.test.ts +0 -1
  60. package/src/lib/api-posts.ts +9 -7
  61. package/src/lib/csp-builder.ts +28 -10
  62. package/src/lib/feed.ts +153 -3
  63. package/src/middleware/__tests__/secure-headers.test.ts +89 -0
  64. package/src/middleware/auth.ts +1 -1
  65. package/src/middleware/secure-headers.ts +47 -1
  66. package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
  67. package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
  68. package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
  69. package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
  70. package/src/node/__tests__/cli-sql-export.test.ts +49 -0
  71. package/src/node/index.ts +1 -0
  72. package/src/preset.css +8 -2
  73. package/src/routes/api/__tests__/settings.test.ts +3 -2
  74. package/src/routes/api/github-sync.tsx +1 -1
  75. package/src/routes/api/settings.ts +4 -1
  76. package/src/routes/auth/signin.tsx +6 -0
  77. package/src/routes/pages/archive.tsx +4 -2
  78. package/src/services/__tests__/post.test.ts +19 -19
  79. package/src/services/__tests__/search.test.ts +0 -1
  80. package/src/services/__tests__/settings.test.ts +22 -3
  81. package/src/services/bootstrap.ts +7 -3
  82. package/src/services/collection.ts +3 -3
  83. package/src/services/export.ts +0 -3
  84. package/src/services/navigation.ts +0 -2
  85. package/src/services/path.ts +1 -38
  86. package/src/services/post.ts +32 -66
  87. package/src/services/search.ts +0 -6
  88. package/src/services/settings.ts +47 -6
  89. package/src/services/site-admin.ts +6 -1
  90. package/src/styles/ui.css +14 -25
  91. package/src/types/entities.ts +0 -1
  92. package/src/ui/color-themes.ts +1 -1
  93. package/src/ui/dash/settings/GeneralContent.tsx +17 -19
  94. package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
  95. package/src/ui/feed/NoteCard.tsx +1 -11
  96. package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
  97. package/src/ui/pages/PostPage.tsx +2 -0
  98. package/bin/commands/collections.js +0 -268
  99. package/bin/commands/media.js +0 -302
  100. package/bin/commands/posts.js +0 -262
  101. package/bin/commands/search.js +0 -53
  102. package/bin/commands/settings.js +0 -93
  103. package/bin/lib/http-api.js +0 -223
  104. package/bin/lib/media-upload.js +0 -206
  105. package/dist/app-CM7sb3xO.js +0 -5
  106. package/dist/client/_assets/client-DDs6NzB3.css +0 -2
  107. package/src/__tests__/bin/content-cli.test.ts +0 -179
  108. package/src/__tests__/bin/media-cli.test.ts +0 -192
  109. /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
  110. /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
@@ -10,12 +10,16 @@
10
10
  import { LitElement, html, nothing } from "lit";
11
11
  import type { Editor } from "@tiptap/core";
12
12
  import { MAX_SITE_NAME_LENGTH } from "../../types.js";
13
+ import {
14
+ getSupportedLocaleEntries,
15
+ getOrBuildEntry,
16
+ type LocaleEntry,
17
+ } from "../../i18n/supported-locales.js";
13
18
  import type {
14
19
  SettingsInitialData,
15
20
  SettingsLabels,
16
21
  SettingsTimezone,
17
22
  SettingsCjkFont,
18
- SettingsLanguage,
19
23
  } from "./settings-types.js";
20
24
  import { showToast } from "../toast.js";
21
25
  import {
@@ -28,7 +32,6 @@ export class JantSettingsGeneral extends LitElement {
28
32
  labels: { type: Object },
29
33
  timezones: { type: Array },
30
34
  cjkFonts: { type: Array, attribute: "cjk-fonts" },
31
- languages: { type: Array },
32
35
  siteNameFallback: { type: String, attribute: "sitename-fallback" },
33
36
  siteDescriptionFallback: {
34
37
  type: String,
@@ -49,6 +52,8 @@ export class JantSettingsGeneral extends LitElement {
49
52
 
50
53
  // Language, CJK & time group
51
54
  _siteLanguage: { state: true },
55
+ _localeOpen: { state: true },
56
+ _localeQuery: { state: true },
52
57
  _cjkSerifFont: { state: true },
53
58
  _timeZone: { state: true },
54
59
  _origLocale: { state: true },
@@ -75,7 +80,6 @@ export class JantSettingsGeneral extends LitElement {
75
80
  declare labels: SettingsLabels;
76
81
  declare timezones: SettingsTimezone[];
77
82
  declare cjkFonts: SettingsCjkFont[];
78
- declare languages: SettingsLanguage[];
79
83
  declare siteNameFallback: string;
80
84
  declare siteDescriptionFallback: string;
81
85
  declare demoMode: boolean;
@@ -97,6 +101,10 @@ export class JantSettingsGeneral extends LitElement {
97
101
 
98
102
  // Language, CJK & time
99
103
  declare _siteLanguage: string;
104
+ /** Whether the locale combobox dropdown is currently open. */
105
+ declare _localeOpen: boolean;
106
+ /** Search query inside the locale combobox. */
107
+ declare _localeQuery: string;
100
108
  declare _cjkSerifFont: string;
101
109
  declare _timeZone: string;
102
110
  declare _origLocale: {
@@ -137,7 +145,6 @@ export class JantSettingsGeneral extends LitElement {
137
145
  this.labels = {} as SettingsLabels;
138
146
  this.timezones = [];
139
147
  this.cjkFonts = [];
140
- this.languages = [];
141
148
  this.siteNameFallback = "";
142
149
  this.siteDescriptionFallback = "";
143
150
  this.demoMode = false;
@@ -157,6 +164,8 @@ export class JantSettingsGeneral extends LitElement {
157
164
  this._siteLoading = false;
158
165
 
159
166
  this._siteLanguage = "en";
167
+ this._localeOpen = false;
168
+ this._localeQuery = "";
160
169
  this._cjkSerifFont = "off";
161
170
  this._timeZone = "UTC";
162
171
  this._origLocale = {
@@ -181,8 +190,16 @@ export class JantSettingsGeneral extends LitElement {
181
190
  this._searchLoading = false;
182
191
  }
183
192
 
193
+ connectedCallback() {
194
+ super.connectedCallback();
195
+ document.addEventListener("click", this._onLocalePickerDocumentClick);
196
+ document.addEventListener("keydown", this._onLocalePickerKeydown);
197
+ }
198
+
184
199
  disconnectedCallback() {
185
200
  super.disconnectedCallback();
201
+ document.removeEventListener("click", this._onLocalePickerDocumentClick);
202
+ document.removeEventListener("keydown", this._onLocalePickerKeydown);
186
203
  this._descEditor?.destroy();
187
204
  this._descEditor = null;
188
205
  this._footerEditor?.destroy();
@@ -387,6 +404,146 @@ export class JantSettingsGeneral extends LitElement {
387
404
  );
388
405
  }
389
406
 
407
+ // ── Locale combobox ────────────────────────────────────────────────
408
+
409
+ private _filteredLocaleEntries(): LocaleEntry[] {
410
+ const all = getSupportedLocaleEntries();
411
+ const query = this._localeQuery.trim().toLowerCase();
412
+ if (!query) return all;
413
+ return all.filter(
414
+ (e) =>
415
+ e.tag.toLowerCase().includes(query) ||
416
+ e.native.toLowerCase().includes(query) ||
417
+ e.english.toLowerCase().includes(query),
418
+ );
419
+ }
420
+
421
+ private _toggleLocalePicker = () => {
422
+ this._localeOpen = !this._localeOpen;
423
+ if (!this._localeOpen) {
424
+ this._localeQuery = "";
425
+ } else {
426
+ // Focus the search input on next paint.
427
+ this.updateComplete.then(() => {
428
+ const input = this.querySelector<HTMLInputElement>(
429
+ "[data-locale-search]",
430
+ );
431
+ input?.focus();
432
+ });
433
+ }
434
+ };
435
+
436
+ private _selectLocale(tag: string) {
437
+ this._siteLanguage = tag;
438
+ this._localeOpen = false;
439
+ this._localeQuery = "";
440
+ this._syncLocaleDirty();
441
+ }
442
+
443
+ private _onLocalePickerDocumentClick = (e: Event) => {
444
+ if (!this._localeOpen) return;
445
+ const target = e.target as Node | null;
446
+ const picker = this.querySelector("[data-locale-picker]");
447
+ if (picker && target && !picker.contains(target)) {
448
+ this._localeOpen = false;
449
+ }
450
+ };
451
+
452
+ private _onLocalePickerKeydown = (e: KeyboardEvent) => {
453
+ if (e.key === "Escape" && this._localeOpen) {
454
+ this._localeOpen = false;
455
+ this._localeQuery = "";
456
+ }
457
+ };
458
+
459
+ private _renderLanguagePicker() {
460
+ const current = getOrBuildEntry(this._siteLanguage || "en");
461
+ const filtered = this._filteredLocaleEntries();
462
+ const searchPlaceholder =
463
+ this.labels.siteLanguageSearchPlaceholder || "Search…";
464
+ const noMatches = this.labels.siteLanguageNoMatches || "No matches.";
465
+
466
+ return html`
467
+ <div class="relative" data-locale-picker>
468
+ <button
469
+ type="button"
470
+ class="input flex w-full items-center justify-between text-left"
471
+ aria-expanded=${this._localeOpen ? "true" : "false"}
472
+ aria-haspopup="listbox"
473
+ aria-labelledby="site-language-label"
474
+ @click=${this._toggleLocalePicker}
475
+ >
476
+ <span class="truncate">
477
+ ${current.native}
478
+ <span class="ml-2 text-xs text-muted-foreground">
479
+ ${current.tag} · ${Math.round(current.coverage * 100)}% translated
480
+ </span>
481
+ </span>
482
+ <span class="ml-2 text-muted-foreground" aria-hidden="true">▾</span>
483
+ </button>
484
+
485
+ ${this._localeOpen
486
+ ? html`
487
+ <div
488
+ class="absolute left-0 right-0 top-full z-10 mt-1 max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md"
489
+ >
490
+ <div class="border-b p-2">
491
+ <input
492
+ type="text"
493
+ class="input w-full"
494
+ data-locale-search
495
+ placeholder=${searchPlaceholder}
496
+ autocomplete="off"
497
+ spellcheck="false"
498
+ .value=${this._localeQuery}
499
+ @input=${(e: Event) => {
500
+ this._localeQuery = (e.target as HTMLInputElement).value;
501
+ }}
502
+ />
503
+ </div>
504
+ <div role="listbox" class="max-h-56 overflow-auto py-1">
505
+ ${filtered.length === 0
506
+ ? html`
507
+ <div class="px-3 py-2 text-sm text-muted-foreground">
508
+ ${noMatches}
509
+ </div>
510
+ `
511
+ : filtered.map(
512
+ (entry) => html`
513
+ <button
514
+ type="button"
515
+ role="option"
516
+ aria-selected=${entry.tag === this._siteLanguage
517
+ ? "true"
518
+ : "false"}
519
+ class=${[
520
+ "flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-accent",
521
+ entry.tag === this._siteLanguage
522
+ ? "bg-accent/60"
523
+ : "",
524
+ ].join(" ")}
525
+ @click=${() => this._selectLocale(entry.tag)}
526
+ >
527
+ <span class="flex flex-col">
528
+ <span>${entry.native}</span>
529
+ <span class="text-xs text-muted-foreground">
530
+ ${entry.tag} · ${entry.english}
531
+ </span>
532
+ </span>
533
+ <span class="text-xs text-muted-foreground">
534
+ ${Math.round(entry.coverage * 100)}% translated
535
+ </span>
536
+ </button>
537
+ `,
538
+ )}
539
+ </div>
540
+ </div>
541
+ `
542
+ : nothing}
543
+ </div>
544
+ `;
545
+ }
546
+
390
547
  // ── Feed group helpers ────────────────────────────────────────────
391
548
 
392
549
  private _syncFeedDirty() {
@@ -652,25 +809,10 @@ export class JantSettingsGeneral extends LitElement {
652
809
  >
653
810
  ${this._renderSectionTitle(this.labels.languageAndTime)}
654
811
  <div class="field">
655
- <label class="label">${this.labels.siteLanguage}</label>
656
- <select
657
- class="select"
658
- @change=${(e: Event) => {
659
- this._siteLanguage = (e.target as HTMLSelectElement).value;
660
- this._syncLocaleDirty();
661
- }}
812
+ <label id="site-language-label" class="label"
813
+ >${this.labels.siteLanguage}</label
662
814
  >
663
- ${this.languages.map(
664
- (lang) => html`
665
- <option
666
- value=${lang.value}
667
- ?selected=${this._siteLanguage === lang.value}
668
- >
669
- ${lang.label}
670
- </option>
671
- `,
672
- )}
673
- </select>
815
+ ${this._renderLanguagePicker()}
674
816
  <p class="text-sm text-muted-foreground mt-1">
675
817
  ${this.labels.siteLanguageHelp}
676
818
  </p>
@@ -43,6 +43,10 @@ export interface SettingsLabels {
43
43
  markdownSupported: string;
44
44
  siteLanguage: string;
45
45
  siteLanguageHelp: string;
46
+ /** Placeholder shown inside the locale combobox search field. */
47
+ siteLanguageSearchPlaceholder: string;
48
+ /** Empty-state message when the search filters out every option. */
49
+ siteLanguageNoMatches: string;
46
50
  cjkFont: string;
47
51
  cjkFontHelp: string;
48
52
  timeZone: string;
@@ -71,12 +75,6 @@ export interface SettingsCjkFont {
71
75
  label: string;
72
76
  }
73
77
 
74
- /** Site language option for the select dropdown */
75
- export interface SettingsLanguage {
76
- value: string;
77
- label: string;
78
- }
79
-
80
78
  export interface SettingsInitialData {
81
79
  siteName: string;
82
80
  siteDescription: string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Authenticated client entry point.
3
3
  *
4
- * Extends the public bundle with editor, upload, and dashboard interactions
4
+ * Extends the public bundle with editor, upload, and settings interactions
5
5
  * that should not be shipped to anonymous page views.
6
6
  */
7
7
 
@@ -74,5 +74,5 @@ describe("demo canonical snapshot", () => {
74
74
  } finally {
75
75
  rmSync(persistDir, { recursive: true, force: true });
76
76
  }
77
- }, 240_000);
77
+ }, 360_000);
78
78
  });
@@ -62,10 +62,7 @@ describe("migration rehearsal", () => {
62
62
  );
63
63
 
64
64
  expect(
65
- queryLocalCount(
66
- persistDir,
67
- "SELECT COUNT(*) AS count FROM post WHERE deleted_at IS NULL",
68
- ),
65
+ queryLocalCount(persistDir, "SELECT COUNT(*) AS count FROM post"),
69
66
  ).toBeGreaterThan(0);
70
67
  expect(
71
68
  queryLocalCount(persistDir, "SELECT COUNT(*) AS count FROM post_fts"),
@@ -73,5 +70,5 @@ describe("migration rehearsal", () => {
73
70
  } finally {
74
71
  rmSync(persistDir, { recursive: true, force: true });
75
72
  }
76
- }, 90_000);
73
+ }, 600_000);
77
74
  });
@@ -0,0 +1,65 @@
1
+ -- Register apple-touch icons in the media table.
2
+ --
3
+ -- Pre-JAN-6, apple-touch icons were uploaded to storage but only referenced
4
+ -- via the SITE_FAVICON_APPLE_TOUCH setting; they had no corresponding media
5
+ -- row. The snapshot logic compensated with a hardcoded list of "storage
6
+ -- setting keys", and the dashboard's media manager couldn't see the file.
7
+ --
8
+ -- JAN-6 makes apple-touch a first-class media entry so snapshot/admin
9
+ -- behavior is uniform with avatar. This backfill creates the missing media
10
+ -- rows for sites that already had apple-touch icons uploaded.
11
+ --
12
+ -- The size column is a placeholder (1). We have no way to read the real byte
13
+ -- length from pure SQL, and the field is only used for storage budgeting
14
+ -- that doesn't apply to favicon assets. Re-uploading the apple-touch icon
15
+ -- through the dashboard refreshes size to the accurate value.
16
+ --
17
+ -- The provider is borrowed from an existing media row in the same site so
18
+ -- the (provider, storage_key) unique index stays consistent with how that
19
+ -- site's other media is tracked.
20
+ INSERT INTO "media" (
21
+ "id",
22
+ "site_id",
23
+ "filename",
24
+ "original_name",
25
+ "mime_type",
26
+ "size",
27
+ "storage_key",
28
+ "provider",
29
+ "position",
30
+ "media_kind",
31
+ "created_at",
32
+ "updated_at"
33
+ )
34
+ SELECT
35
+ 'med_apt_' || substr(s."site_id", 5),
36
+ s."site_id",
37
+ 'apple-touch-icon.png',
38
+ 'apple-touch-icon.png',
39
+ 'image/png',
40
+ 1,
41
+ s."value",
42
+ COALESCE(
43
+ (
44
+ SELECT m."provider"
45
+ FROM "media" m
46
+ WHERE m."site_id" = s."site_id"
47
+ ORDER BY m."created_at"
48
+ LIMIT 1
49
+ ),
50
+ 'r2'
51
+ ),
52
+ 'a0',
53
+ 'image',
54
+ s."updated_at",
55
+ s."updated_at"
56
+ FROM "site_setting" s
57
+ WHERE s."key" = 'SITE_FAVICON_APPLE_TOUCH'
58
+ AND s."value" IS NOT NULL
59
+ AND trim(s."value") <> ''
60
+ AND NOT EXISTS (
61
+ SELECT 1
62
+ FROM "media" m
63
+ WHERE m."site_id" = s."site_id"
64
+ AND m."storage_key" = s."value"
65
+ );
@@ -0,0 +1,16 @@
1
+ DELETE FROM `post` WHERE `deleted_at` IS NOT NULL;--> statement-breakpoint
2
+ DROP INDEX `idx_post_site_thread_live_created`;--> statement-breakpoint
3
+ DROP INDEX `idx_post_site_status_deleted_published`;--> statement-breakpoint
4
+ DROP INDEX `idx_post_site_status_deleted_activity`;--> statement-breakpoint
5
+ DROP INDEX `idx_post_site_root_live_published_activity`;--> statement-breakpoint
6
+ DROP INDEX `idx_post_site_root_live_draft_updated`;--> statement-breakpoint
7
+ DROP INDEX `idx_post_site_reply_live_thread_created`;--> statement-breakpoint
8
+ DROP INDEX `idx_post_site_featured_live_featured_at`;--> statement-breakpoint
9
+ CREATE INDEX `idx_post_site_thread_created` ON `post` (`site_id`,`thread_id`,`created_at`,`id`);--> statement-breakpoint
10
+ CREATE INDEX `idx_post_site_status_published` ON `post` (`site_id`,`status`,`published_at`);--> statement-breakpoint
11
+ CREATE INDEX `idx_post_site_status_activity` ON `post` (`site_id`,`status`,`last_activity_at`);--> statement-breakpoint
12
+ CREATE INDEX `idx_post_site_root_published_activity` ON `post` (`site_id`,`last_activity_at`,`id`) WHERE "post"."reply_to_id" IS NULL AND "post"."status" = 'published';--> statement-breakpoint
13
+ CREATE INDEX `idx_post_site_root_draft_updated` ON `post` (`site_id`,`updated_at`,`id`) WHERE "post"."reply_to_id" IS NULL AND "post"."status" = 'draft';--> statement-breakpoint
14
+ CREATE INDEX `idx_post_site_reply_thread_created` ON `post` (`site_id`,`thread_id`,`created_at`,`id`) WHERE "post"."reply_to_id" IS NOT NULL AND "post"."status" = 'published';--> statement-breakpoint
15
+ CREATE INDEX `idx_post_site_featured_featured_at` ON `post` (`site_id`,`featured_at`,`thread_id`,`id`) WHERE "post"."status" = 'published' AND "post"."featured_at" IS NOT NULL;--> statement-breakpoint
16
+ ALTER TABLE `post` DROP COLUMN `deleted_at`;