Record the Studio schema migration atomically and support safe retries
waterfall-wiki CI and deployment / verify-and-deploy (push) Has been cancelled

This commit is contained in:
2026-10-27 15:56:28 +08:00
parent 054e993043
commit 5427df8ff3
2 changed files with 6 additions and 2 deletions
@@ -1,4 +1,5 @@
CREATE TABLE studio_designs (
BEGIN;
CREATE TABLE IF NOT EXISTS studio_designs (
id uuid PRIMARY KEY,
layout_id text NOT NULL CHECK (layout_id ~ '^ref-(0[1-9]|1[0-9]|2[0-7])$'),
name text NOT NULL CHECK (char_length(name) BETWEEN 1 AND 120),
@@ -7,4 +8,6 @@ CREATE TABLE studio_designs (
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX studio_designs_layout_created ON studio_designs(layout_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS studio_designs_layout_created ON studio_designs(layout_id,created_at DESC,id DESC);
INSERT INTO schema_migrations(version) VALUES('009_studio_designs');
COMMIT;
@@ -57,6 +57,7 @@ test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processi
assert.equal((await db().query("SELECT count(*)::integer AS n FROM assets WHERE kind='audio' AND derivation='audio_track'")).rows[0].n,1,'Video soundtracks are ordinary reusable assets');
const audio=join(dir,'audio.wav');await run('ffmpeg',['-y','-v','error','-f','lavfi','-i','sine=frequency=220','-t','0.2',audio]);await upload('audio',[{data:await readFile(audio),name:'audio.wav',mime:'audio/wav'}]);assert.equal((await request('/api/feed?section=audio')).posts.length,1);
for(const removed of ['/library/assets','/library/tags'])assert.equal((await fetch(base+removed)).status,404);
assert.equal((await db().query("SELECT 1 FROM schema_migrations WHERE version='009_studio_designs'")).rowCount,1,'Studio migration must be recorded for repeat deployments');
// Studio owns no second database: designs use this same isolated PostgreSQL.
for(let i=1;i<=27;i++){const layoutId='ref-'+String(i).padStart(2,'0'),saved=await request('/api/studio/designs',{layoutId,name:'布局 '+i,parameters:{}});assert.equal(saved.design.layoutId,layoutId);assert.equal(saved.design.revision,1);const read=await request('/api/studio/designs/'+saved.design.id);assert.deepEqual(read.design.parameters,{texts:{},images:{},decorations:{}});}
const design=await request('/api/studio/designs',{layoutId:'ref-01',name:'可修改效果',parameters:{texts:{T01:{value:'新的标题'}},images:{I01:{assetId:first.assets[0],fit:'contain'}}}});