@stacksjs/defaults 0.74.33 → 0.74.34

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.
@@ -0,0 +1,123 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * Dashboard metadata for one file on one disk (stacksjs/stacks#2577).
6
+ *
7
+ * Every file-manager operation shipped before this maps to a `StorageAdapter`
8
+ * method - `write`, `list`, `moveFile`, `changeVisibility`, `deleteFile`. A disk
9
+ * knows a path, some bytes, a size and an ACL. It does not know that somebody
10
+ * starred a file, and there is nowhere on a disk to write that down: extended
11
+ * attributes do not survive a copy and cost a syscall per file, and S3 object
12
+ * metadata is set at write time, so starring a 2 GB video would rewrite 2 GB.
13
+ * Both are a database row wearing a disguise, so this is the row.
14
+ *
15
+ * ## The disk is authoritative, this table is advisory
16
+ *
17
+ * That is the question #2577 asks to settle first, and it decides everything
18
+ * else. A bucket several systems write to changes without the dashboard's
19
+ * knowledge, so a table that claimed to describe its contents would be wrong
20
+ * within a day of being right. Instead:
21
+ *
22
+ * - The listing comes from the disk and is joined to these rows. A path with no
23
+ * row is a file with no stars, which is the normal case and needs no row.
24
+ * - A row whose path no longer exists is an orphan and is simply not shown. The
25
+ * listing pass already enumerates every path in the subtree, so it sweeps the
26
+ * orphans it can prove are orphans - the ones under a prefix it just walked -
27
+ * at no extra cost. Nothing walks the whole disk to garbage collect.
28
+ * - Renames and deletes made THROUGH the dashboard reconcile eagerly, so a
29
+ * starred file keeps its star the moment it moves rather than waiting for a
30
+ * sweep. A folder rename is a prefix update, because moving a folder moves
31
+ * every file beneath it.
32
+ *
33
+ * The consequence worth stating: a file renamed outside the dashboard loses its
34
+ * metadata, because nothing connects the old path to the new one. That is not
35
+ * a gap to close later - a copy and a rename are indistinguishable to a bucket
36
+ * listing, so any reconciliation would be guessing.
37
+ *
38
+ * Keyed by `(disk, path)` rather than by a file id, because a disk has no file
39
+ * ids to key by.
40
+ *
41
+ * ## Tags are the existing vocabulary, reached the way the CMS reaches it
42
+ *
43
+ * #2577 asks for the existing vocabulary rather than a second one. That is the
44
+ * `tags` table, joined through the `taggable_models` pivot, with
45
+ * `taggable_type` keeping one model's attachments away from another's - which
46
+ * is what the dashboard's own tag manager writes and reads.
47
+ *
48
+ * `file-metadata-store.ts` queries it directly rather than declaring a
49
+ * `belongsToMany` here, because every operation is a set operation over a
50
+ * subtree and the relation would do them a row at a time. The relation would
51
+ * resolve correctly, though: `taggable_models.tag_id` is a `tags` id, which
52
+ * took stacksjs/stacks#2579 to establish - the migration comment and four
53
+ * queries in `@stacksjs/cms` said `taggables`, a different table belonging to a
54
+ * different mechanism.
55
+ */
56
+ export default defineModel({
57
+ name: 'StorageItem',
58
+ table: 'storage_items',
59
+ primaryKey: 'id',
60
+ autoIncrement: true,
61
+
62
+ /**
63
+ * One row per file, enforced rather than assumed.
64
+ *
65
+ * Every write here is a get-or-create keyed on this pair, and a duplicate
66
+ * would not be a visible bug - it would be a file that is starred and also
67
+ * not starred, depending which row the query happened to read first.
68
+ */
69
+ indexes: [
70
+ {
71
+ name: 'storage_items_disk_path_unique',
72
+ columns: ['disk', 'path'],
73
+ unique: true,
74
+ },
75
+ ],
76
+
77
+ traits: {
78
+ useUuid: true,
79
+ useTimestamps: true,
80
+
81
+ useSearch: {
82
+ displayable: ['id', 'disk', 'path', 'favorite'],
83
+ searchable: ['path'],
84
+ sortable: ['path', 'createdAt'],
85
+ filterable: ['disk', 'favorite'],
86
+ },
87
+
88
+ useSeeder: { count: 0 },
89
+ },
90
+
91
+ attributes: {
92
+ disk: {
93
+ order: 1,
94
+ fillable: true,
95
+ // The disk NAME, not its driver: two disks can point at one bucket with
96
+ // different prefixes, and they are different namespaces to the dashboard.
97
+ validation: { rule: schema.string().required().min(1).max(64) },
98
+ factory: () => 'public',
99
+ },
100
+
101
+ path: {
102
+ order: 2,
103
+ fillable: true,
104
+ // Disk-relative and without a leading slash, exactly as the file manager
105
+ // reports it, so a lookup is an equality match rather than a normalization
106
+ // problem at every call site.
107
+ validation: { rule: schema.string().required().min(1).max(2048) },
108
+ factory: faker => `${faker.system.fileName()}`,
109
+ },
110
+
111
+ favorite: {
112
+ order: 3,
113
+ fillable: true,
114
+ default: false,
115
+ validation: { rule: schema.boolean() },
116
+ factory: () => false,
117
+ },
118
+ },
119
+
120
+ // Managed from the file manager, not as a top-level model row: a list of
121
+ // (disk, path, favorite) tuples is not a thing anybody wants to browse.
122
+ dashboard: { enabled: false },
123
+ } as const)
@@ -0,0 +1,134 @@
1
+ import { defineModel } from '@stacksjs/orm'
2
+ import { schema } from '@stacksjs/validation'
3
+
4
+ /**
5
+ * One unit of background processing for one file (stacksjs/stacks#2578).
6
+ *
7
+ * The three things #245 asked for that could not happen inside a request -
8
+ * image optimization, video transcode, AI tagging - share the same shape:
9
+ * dispatch work and show what happened to it. Transcoding is minutes, not
10
+ * milliseconds; a vision call is a round trip to a third party. An upload
11
+ * handler that waits for either is an upload handler that times out.
12
+ *
13
+ * ## Why this is not a column on `StorageItem`
14
+ *
15
+ * A file can have three of these at once and they succeed and fail
16
+ * independently - a video whose transcode finished and whose tagging failed is
17
+ * a normal state, and a single `processing` column cannot say it. The file
18
+ * manager aggregates these rows into the one word a UI shows; the rows are what
19
+ * a retry and an error message need.
20
+ *
21
+ * It is also keyed by `(disk, path, kind)` rather than by a `storage_items` id,
22
+ * deliberately. A `storage_items` row exists only while somebody has starred or
23
+ * tagged the file - it is deleted when it has nothing left to say - and most
24
+ * uploads are neither. Hanging a task off it would mean creating a metadata row
25
+ * for every upload just to have somewhere to put the task.
26
+ *
27
+ * Same reconciliation rules as `storage_items`, through the same store: a
28
+ * rename moves these rows, a delete forgets them, and a completed listing
29
+ * sweeps the ones whose file is gone.
30
+ */
31
+ export default defineModel({
32
+ name: 'StorageItemTask',
33
+ table: 'storage_item_tasks',
34
+ primaryKey: 'id',
35
+ autoIncrement: true,
36
+
37
+ /**
38
+ * One task per kind per file.
39
+ *
40
+ * Re-running replaces the row rather than adding a second, so "what happened
41
+ * to the transcode" has one answer. The history of previous attempts is the
42
+ * queue's to keep, not this table's.
43
+ */
44
+ indexes: [
45
+ {
46
+ name: 'storage_item_tasks_disk_path_kind_unique',
47
+ columns: ['disk', 'path', 'kind'],
48
+ unique: true,
49
+ },
50
+ ],
51
+
52
+ traits: {
53
+ useUuid: true,
54
+ useTimestamps: true,
55
+
56
+ useSearch: {
57
+ displayable: ['id', 'disk', 'path', 'kind', 'state'],
58
+ searchable: ['path'],
59
+ sortable: ['path', 'createdAt'],
60
+ filterable: ['disk', 'kind', 'state'],
61
+ },
62
+
63
+ useSeeder: { count: 0 },
64
+ },
65
+
66
+ attributes: {
67
+ disk: {
68
+ order: 1,
69
+ fillable: true,
70
+ validation: { rule: schema.string().required().min(1).max(64) },
71
+ factory: () => 'public',
72
+ },
73
+
74
+ path: {
75
+ order: 2,
76
+ fillable: true,
77
+ validation: { rule: schema.string().required().min(1).max(2048) },
78
+ factory: faker => faker.system.fileName(),
79
+ },
80
+
81
+ kind: {
82
+ order: 3,
83
+ fillable: true,
84
+ // `optimize` (images), `transcode` (video), `tag` (AI). An enum rather
85
+ // than free text so a typo in a dispatch is a validation error instead of
86
+ // a task nothing will ever run.
87
+ validation: { rule: schema.enum(['optimize', 'transcode', 'tag']) },
88
+ factory: () => 'optimize',
89
+ },
90
+
91
+ state: {
92
+ order: 4,
93
+ fillable: true,
94
+ default: 'queued',
95
+ validation: { rule: schema.enum(['queued', 'running', 'done', 'failed']) },
96
+ factory: () => 'queued',
97
+ },
98
+
99
+ attempts: {
100
+ order: 5,
101
+ fillable: true,
102
+ default: 0,
103
+ // Counted here as well as in the queue, because the queue's count is gone
104
+ // once the job leaves it and this is what the dashboard reads.
105
+ validation: { rule: schema.number() },
106
+ factory: () => 0,
107
+ },
108
+
109
+ error: {
110
+ order: 6,
111
+ fillable: true,
112
+ // The failure as the worker saw it. Truncated by the writer rather than
113
+ // by the column, so a stack trace does not silently lose its first line.
114
+ validation: { rule: schema.string().max(2000) },
115
+ factory: () => '',
116
+ },
117
+
118
+ startedAt: {
119
+ order: 7,
120
+ fillable: true,
121
+ validation: { rule: schema.string() },
122
+ factory: () => '',
123
+ },
124
+
125
+ finishedAt: {
126
+ order: 8,
127
+ fillable: true,
128
+ validation: { rule: schema.string() },
129
+ factory: () => '',
130
+ },
131
+ },
132
+
133
+ dashboard: { enabled: false },
134
+ } as const)
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.74.33",
5
+ "version": "0.74.34",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.33",
5
+ "version": "0.74.34",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -55,7 +55,7 @@
55
55
  "dependencies": {
56
56
  "@iconify-json/f7": "^1.2.2",
57
57
  "@iconify-json/hugeicons": "^1.2.27",
58
- "@stacksjs/mobile": "^0.74.33",
58
+ "@stacksjs/mobile": "^0.74.34",
59
59
  "@stacksjs/sanitizer": "^0.2.113",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -247,6 +247,16 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
247
247
  guard(route.post('/files/uploads', 'Actions/Dashboard/Content/FileUploadAction'))
248
248
  guard(route.patch('/files', 'Actions/Dashboard/Content/FileRenameAction'))
249
249
  guard(route.put('/files/visibility', 'Actions/Dashboard/Content/FileVisibilityAction'))
250
+ // Favourites and tags are the metadata layer, not a storage operation: a disk
251
+ // has nowhere to record either, so both write `storage_items` and the listing
252
+ // above joins them back on (stacksjs/stacks#2577).
253
+ guard(route.put('/files/favorite', 'Actions/Dashboard/Content/FileFavoriteAction'))
254
+ guard(route.put('/files/tags', 'Actions/Dashboard/Content/FileTagsAction'))
255
+ // Re-running the background processing (stacksjs/stacks#2578). Not optional:
256
+ // the first version of any of these produces output somebody wants
257
+ // regenerated - a better ladder, a model that has improved, an optimization
258
+ // that ran before a preset changed.
259
+ guard(route.post('/files/reprocess', 'Actions/Dashboard/Content/FileReprocessAction'))
250
260
  guard(route.post('/files/duplicates', 'Actions/Dashboard/Content/FileDuplicateAction'))
251
261
  guard(route.delete('/files', 'Actions/Dashboard/Content/FileDestroyAction'))
252
262
 
@@ -0,0 +1,109 @@
1
+ # The one dependency bot a generated app gets.
2
+ #
3
+ # The template used to ship a `renovate.json` instead, which opted every new app
4
+ # into a bot the framework's own policy does not use (AGENTS.md: "buddy-bot
5
+ # handles dependency updates, not renovatebot"). Running both is what
6
+ # stacksjs/stacks#2574 is about: the same bump arrives twice, two dashboards
7
+ # disagree, and a human picks a winner with nothing recording why.
8
+ #
9
+ # This is the app-shaped version of the framework's own buddy-bot workflow -
10
+ # the three scheduled jobs and the manual trigger, without the framework's
11
+ # release plumbing. Configure it in `config/buddy-bot.ts`.
12
+ name: Buddy Bot
13
+
14
+ on:
15
+ schedule:
16
+ # Look for updates every two hours, then refresh the dashboard 15 minutes
17
+ # later so it reflects the pull requests that run just opened.
18
+ - cron: '0 */2 * * *'
19
+ - cron: '15 */2 * * *'
20
+ # Daily: close pull requests whose update already landed, and clean up the
21
+ # branches left behind.
22
+ - cron: '0 4 * * *'
23
+
24
+ workflow_dispatch:
25
+ inputs:
26
+ job:
27
+ description: Which job to run
28
+ required: false
29
+ default: all
30
+ type: choice
31
+ options:
32
+ - all
33
+ - check
34
+ - update
35
+ - dashboard
36
+ strategy:
37
+ description: Update strategy
38
+ required: false
39
+ default: patch
40
+ type: choice
41
+ options:
42
+ - all
43
+ - major
44
+ - minor
45
+ - patch
46
+ dry_run:
47
+ description: Dry run (preview only)
48
+ required: false
49
+ default: false
50
+ type: boolean
51
+
52
+ env:
53
+ # The built-in token is enough for commits, pull requests and issues.
54
+ # BUDDY_BOT_TOKEN (a PAT with `repo` and `workflow` scope) is only needed if
55
+ # you want buddy-bot to update workflow files themselves.
56
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
57
+ BUDDY_BOT_TOKEN: ${{ secrets.BUDDY_BOT_TOKEN }}
58
+
59
+ permissions:
60
+ contents: write
61
+ pull-requests: write
62
+ issues: write
63
+
64
+ # Serialize runs so a scheduled tick and a manual one cannot race on the same
65
+ # branches. `cancel-in-progress: false` lets the running job finish rather than
66
+ # being killed part-way through a commit.
67
+ concurrency:
68
+ group: buddy-bot-${{ github.ref }}
69
+ cancel-in-progress: false
70
+
71
+ jobs:
72
+ buddy-bot:
73
+ runs-on: ubuntu-latest
74
+ timeout-minutes: 30
75
+
76
+ steps:
77
+ - name: Checkout Code
78
+ uses: actions/checkout@v4
79
+ with:
80
+ token: ${{ secrets.GITHUB_TOKEN }}
81
+ # Rebasing an open branch needs the history it was cut from.
82
+ fetch-depth: 0
83
+ persist-credentials: true
84
+
85
+ - name: Setup Pantry
86
+ uses: pantry-pm/pantry/packages/action@fa35547da630b14beda5b26a2625490864d5c963 # v0.11.60
87
+ with:
88
+ version: '0.11.60'
89
+ install: 'false'
90
+
91
+ - name: Install Dependencies
92
+ run: bun install
93
+
94
+ - name: Configure Git
95
+ run: |
96
+ git config --global user.name "github-actions[bot]"
97
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
98
+
99
+ - name: Open and refresh update pull requests
100
+ if: ${{ github.event.schedule == '0 */2 * * *' || (github.event_name == 'workflow_dispatch' && contains(fromJSON('["all", "update"]'), inputs.job)) }}
101
+ run: bunx buddy-bot update --strategy "${{ inputs.strategy || 'patch' }}" ${{ inputs.dry_run && '--dry-run' || '' }}
102
+
103
+ - name: Update the dependency dashboard
104
+ if: ${{ github.event.schedule == '15 */2 * * *' || (github.event_name == 'workflow_dispatch' && contains(fromJSON('["all", "dashboard"]'), inputs.job)) }}
105
+ run: bunx buddy-bot dashboard ${{ inputs.dry_run && '--dry-run' || '' }}
106
+
107
+ - name: Close landed pull requests and clean up branches
108
+ if: ${{ github.event.schedule == '0 4 * * *' || (github.event_name == 'workflow_dispatch' && contains(fromJSON('["all", "check"]'), inputs.job)) }}
109
+ run: bunx buddy-bot update-check ${{ inputs.dry_run && '--dry-run' || '' }}
@@ -30,7 +30,7 @@ jobs:
30
30
  uses: actions/checkout@v4
31
31
 
32
32
  - name: Setup Pantry
33
- uses: pantry-pm/pantry/packages/action@c1417d98f5613290bc3b1f96d35086b834684659 # v0.10.47 + the socket-hang-up retry fix
33
+ uses: pantry-pm/pantry/packages/action@fa35547da630b14beda5b26a2625490864d5c963 # v0.11.60
34
34
  with:
35
35
  install: 'false'
36
36
 
@@ -61,7 +61,7 @@ jobs:
61
61
  uses: actions/checkout@v4
62
62
 
63
63
  - name: Setup Pantry
64
- uses: pantry-pm/pantry/packages/action@c1417d98f5613290bc3b1f96d35086b834684659 # v0.10.47 + the socket-hang-up retry fix
64
+ uses: pantry-pm/pantry/packages/action@fa35547da630b14beda5b26a2625490864d5c963 # v0.11.60
65
65
  with:
66
66
  install: 'false'
67
67
 
@@ -95,7 +95,7 @@ jobs:
95
95
  uses: actions/checkout@v4
96
96
 
97
97
  - name: Setup Pantry
98
- uses: pantry-pm/pantry/packages/action@c1417d98f5613290bc3b1f96d35086b834684659 # v0.10.47 + the socket-hang-up retry fix
98
+ uses: pantry-pm/pantry/packages/action@fa35547da630b14beda5b26a2625490864d5c963 # v0.11.60
99
99
  with:
100
100
  install: 'false'
101
101
 
@@ -21,7 +21,7 @@ jobs:
21
21
  fetch-depth: 0
22
22
 
23
23
  - name: Setup Pantry
24
- uses: pantry-pm/pantry/packages/action@c1417d98f5613290bc3b1f96d35086b834684659 # v0.10.47 + the socket-hang-up retry fix
24
+ uses: pantry-pm/pantry/packages/action@fa35547da630b14beda5b26a2625490864d5c963 # v0.11.60
25
25
  with:
26
26
  install: 'false'
27
27
 
@@ -1,5 +0,0 @@
1
- {
2
- "extends": [
3
- "github>ow3org/renovate-config"
4
- ]
5
- }