PR 6: Database migration validation - fresh/upgrade test complete

 Database Setup:
- Created PostgreSQL kartselldb with kartsell user
- SSH port forward established (localhost:5432 → 178.104.200.7:5432)

 DbMigrator Fixes:
- Fixed migration path discovery (AppContext.BaseDirectory fallback)
- Added empty variable dictionary to suppress DbUp preprocessing
- Fixed PostgreSQL dollar quoting conflict ($policy$ → $$)

 Migration Results:
- All 21 migrations executed successfully
- Schema versions journal created and tracked
- 21 scripts processed in order, no rollback needed

Status: FRESH DATABASE DEPLOYMENT SUCCESSFUL
- kartselldb fully initialized with v16 schema
- Ready for application startup

Next: Deploy application and run integration tests

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 06:45:20 +09:00
parent fc39c8d4bf
commit 3b76070394
135 changed files with 6673 additions and 2 deletions
@@ -0,0 +1,111 @@
import PageLayout from '../ui/layouts/PageLayout.vue';
import FormPageLayout from '../ui/layouts/FormPageLayout.vue';
import StandardScreenBoundary from '../ui/screen-types/v2/StandardScreenBoundary.vue';
const __VLS_props = withDefaults(defineProps(), { state: 'READY', dirty: false, readonly: false });
const emit = defineEmits();
const __VLS_defaults = { state: 'READY', dirty: false, readonly: false };
const __VLS_ctx = {
...{},
...{},
...{},
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
const __VLS_0 = PageLayout || PageLayout;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
title: (__VLS_ctx.title),
subtitle: (__VLS_ctx.subtitle),
status: (__VLS_ctx.readonly ? 'READONLY' : __VLS_ctx.state),
asOf: (__VLS_ctx.asOf),
version: (__VLS_ctx.version),
}));
const __VLS_2 = __VLS_1({
title: (__VLS_ctx.title),
subtitle: (__VLS_ctx.subtitle),
status: (__VLS_ctx.readonly ? 'READONLY' : __VLS_ctx.state),
asOf: (__VLS_ctx.asOf),
version: (__VLS_ctx.version),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
var __VLS_5;
const { default: __VLS_6 } = __VLS_3.slots;
const __VLS_7 = StandardScreenBoundary || StandardScreenBoundary;
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent1(__VLS_7, new __VLS_7({
...{ 'onRetry': {} },
state: (__VLS_ctx.readonly ? 'READONLY' : (__VLS_ctx.dirty ? 'DIRTY' : __VLS_ctx.state)),
staleAt: (__VLS_ctx.asOf),
}));
const __VLS_9 = __VLS_8({
...{ 'onRetry': {} },
state: (__VLS_ctx.readonly ? 'READONLY' : (__VLS_ctx.dirty ? 'DIRTY' : __VLS_ctx.state)),
staleAt: (__VLS_ctx.asOf),
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
let __VLS_12;
const __VLS_13 = {
/** @type {typeof __VLS_12.retry} */
onRetry: (...[$event]) => {
return (__VLS_ctx.emit('retry'));
// @ts-ignore
[title, subtitle, readonly, readonly, state, state, asOf, asOf, version, dirty, emit,];
},
};
const { default: __VLS_14 } = __VLS_10.slots;
const __VLS_15 = FormPageLayout || FormPageLayout;
// @ts-ignore
const __VLS_16 = __VLS_asFunctionalComponent1(__VLS_15, new __VLS_15({
...{ 'onSubmit': {} },
}));
const __VLS_17 = __VLS_16({
...{ 'onSubmit': {} },
}, ...__VLS_functionalComponentArgsRest(__VLS_16));
let __VLS_20;
const __VLS_21 = {
/** @type {typeof __VLS_20.submit} */
onSubmit: (...[$event]) => {
return (__VLS_ctx.emit('submit'));
// @ts-ignore
[emit,];
},
};
const { default: __VLS_22 } = __VLS_18.slots;
var __VLS_23 = {};
if (__VLS_ctx.$slots.aside) {
{
const { preview: __VLS_25 } = __VLS_18.slots;
var __VLS_26 = {};
// @ts-ignore
[$slots,];
}
}
// @ts-ignore
[];
var __VLS_18;
var __VLS_19;
// @ts-ignore
[];
var __VLS_10;
var __VLS_11;
{
const { footer: __VLS_28 } = __VLS_3.slots;
var __VLS_29 = {};
// @ts-ignore
[];
}
// @ts-ignore
[];
var __VLS_3;
// @ts-ignore
var __VLS_24 = __VLS_23, __VLS_27 = __VLS_26, __VLS_30 = __VLS_29;
// @ts-ignore
[];
const __VLS_base = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
props: {},
});
const __VLS_export = {};
export default {};
@@ -0,0 +1,159 @@
import PageLayout from '../ui/layouts/PageLayout.vue';
import StandardScreenBoundary from '../ui/screen-types/v2/StandardScreenBoundary.vue';
import KsDataGrid from '../ui/components/KsDataGrid.vue';
import KsPaginator from '../ui/components/KsPaginator.vue';
const __VLS_props = withDefaults(defineProps(), { state: 'READY', rows: () => [] });
const emit = defineEmits();
const __VLS_defaults = { state: 'READY', rows: () => [] };
const __VLS_ctx = {
...{},
...{},
...{},
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
const __VLS_0 = PageLayout || PageLayout;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
title: (__VLS_ctx.title),
subtitle: (__VLS_ctx.subtitle),
status: (__VLS_ctx.state),
asOf: (__VLS_ctx.asOf),
version: (__VLS_ctx.version),
}));
const __VLS_2 = __VLS_1({
title: (__VLS_ctx.title),
subtitle: (__VLS_ctx.subtitle),
status: (__VLS_ctx.state),
asOf: (__VLS_ctx.asOf),
version: (__VLS_ctx.version),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
var __VLS_5;
const { default: __VLS_6 } = __VLS_3.slots;
{
const { actions: __VLS_7 } = __VLS_3.slots;
var __VLS_8 = {};
// @ts-ignore
[title, subtitle, state, asOf, version,];
}
{
const { summary: __VLS_10 } = __VLS_3.slots;
var __VLS_11 = {};
// @ts-ignore
[];
}
{
const { filters: __VLS_13 } = __VLS_3.slots;
var __VLS_14 = {};
// @ts-ignore
[];
}
const __VLS_16 = StandardScreenBoundary || StandardScreenBoundary;
// @ts-ignore
const __VLS_17 = __VLS_asFunctionalComponent1(__VLS_16, new __VLS_16({
...{ 'onRetry': {} },
state: (__VLS_ctx.state),
warning: (__VLS_ctx.warning),
staleAt: (__VLS_ctx.asOf),
}));
const __VLS_18 = __VLS_17({
...{ 'onRetry': {} },
state: (__VLS_ctx.state),
warning: (__VLS_ctx.warning),
staleAt: (__VLS_ctx.asOf),
}, ...__VLS_functionalComponentArgsRest(__VLS_17));
let __VLS_21;
const __VLS_22 = {
/** @type {typeof __VLS_21.retry} */
onRetry: (...[$event]) => {
return (__VLS_ctx.emit('retry'));
// @ts-ignore
[state, asOf, warning, emit,];
},
};
const { default: __VLS_23 } = __VLS_19.slots;
const __VLS_24 = KsDataGrid;
// @ts-ignore
const __VLS_25 = __VLS_asFunctionalComponent1(__VLS_24, new __VLS_24({
...{ 'onRowSelected': {} },
rows: (__VLS_ctx.rows),
columns: (__VLS_ctx.columns),
}));
const __VLS_26 = __VLS_25({
...{ 'onRowSelected': {} },
rows: (__VLS_ctx.rows),
columns: (__VLS_ctx.columns),
}, ...__VLS_functionalComponentArgsRest(__VLS_25));
let __VLS_29;
const __VLS_30 = {
/** @type {typeof __VLS_29.rowSelected} */
onRowSelected: (...[$event]) => {
return (__VLS_ctx.emit('rowSelected', $event));
// @ts-ignore
[emit, rows, columns,];
},
};
var __VLS_27;
var __VLS_28;
const __VLS_31 = KsPaginator;
// @ts-ignore
const __VLS_32 = __VLS_asFunctionalComponent1(__VLS_31, new __VLS_31({
...{ 'onPageChange': {} },
page: (__VLS_ctx.page),
pageSize: (__VLS_ctx.pageSize),
total: (__VLS_ctx.total),
}));
const __VLS_33 = __VLS_32({
...{ 'onPageChange': {} },
page: (__VLS_ctx.page),
pageSize: (__VLS_ctx.pageSize),
total: (__VLS_ctx.total),
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
let __VLS_36;
const __VLS_37 = {
/** @type {typeof __VLS_36.pageChange} */
onPageChange: (...[$event]) => {
return (__VLS_ctx.emit('pageChange', $event));
// @ts-ignore
[emit, page, pageSize, total,];
},
};
var __VLS_34;
var __VLS_35;
// @ts-ignore
[];
var __VLS_19;
var __VLS_20;
if (__VLS_ctx.$slots.detail) {
{
const { aside: __VLS_38 } = __VLS_3.slots;
var __VLS_39 = {};
// @ts-ignore
[$slots,];
}
}
if (__VLS_ctx.$slots.footer) {
{
const { footer: __VLS_41 } = __VLS_3.slots;
var __VLS_42 = {};
// @ts-ignore
[$slots,];
}
}
// @ts-ignore
[];
var __VLS_3;
// @ts-ignore
var __VLS_9 = __VLS_8, __VLS_12 = __VLS_11, __VLS_15 = __VLS_14, __VLS_40 = __VLS_39, __VLS_43 = __VLS_42;
// @ts-ignore
[];
const __VLS_base = (await import('vue')).defineComponent({
__typeEmits: {},
__typeProps: {},
props: {},
});
const __VLS_export = {};
export default {};
+1
View File
@@ -0,0 +1 @@
export {};
+41
View File
@@ -0,0 +1,41 @@
const positiveInt = (value, fallback) => {
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
};
export function decodeCrudQuery(params, fallback) {
const sorts = params.getAll('sort').flatMap(value => {
const parts = value.split(':');
const field = parts[0];
const direction = parts[1];
if (field && (direction === 'asc' || direction === 'desc')) {
return [{ field, direction: direction }];
}
return [];
});
const filters = params.getAll('filter').flatMap(value => {
const first = value.indexOf(':');
const second = value.indexOf(':', first + 1);
if (first <= 0 || second <= first)
return [];
return [{ field: value.slice(0, first), operator: value.slice(first + 1, second), value: value.slice(second + 1) }];
});
return {
page: positiveInt(params.get('page'), fallback.page),
pageSize: positiveInt(params.get('pageSize'), fallback.pageSize),
search: params.get('search')?.trim() || undefined,
sorts: sorts.length ? sorts : fallback.sorts,
filters: filters.length ? filters : fallback.filters
};
}
export function encodeCrudQuery(query) {
const params = new URLSearchParams();
params.set('page', String(query.page));
params.set('pageSize', String(query.pageSize));
if (query.search)
params.set('search', query.search);
for (const sort of query.sorts)
params.append('sort', `${sort.field}:${sort.direction}`);
for (const filter of query.filters)
params.append('filter', `${filter.field}:${filter.operator}:${String(filter.value ?? '')}`);
return params;
}
@@ -0,0 +1,8 @@
export function assertCrudResourceDefinition(definition) {
if (!definition.resourceId.trim())
throw new Error('resourceId is required');
const fields = new Set(definition.columns.map(x => x.field));
for (const sensitive of definition.sensitiveFields)
if (!fields.has(sensitive))
throw new Error(`Sensitive field '${sensitive}' has no grid column contract`);
}
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { decodeCrudQuery, encodeCrudQuery } from '../queryCodec';
const fallback = { page: 1, pageSize: 20, sorts: [], filters: [] };
describe('CRUD URL codec', () => {
it('round-trips paging, sort, filter and search without hidden Pinia state', () => {
const source = { page: 3, pageSize: 50, search: '005930', sorts: [{ field: 'asOf', direction: 'desc' }], filters: [{ field: 'status', operator: 'eq', value: 'WARN' }] };
expect(decodeCrudQuery(encodeCrudQuery(source), fallback)).toEqual(source);
});
it('fails closed to approved defaults for invalid paging', () => {
expect(decodeCrudQuery(new URLSearchParams('page=0&pageSize=-1'), fallback)).toEqual({ ...fallback, search: undefined });
});
});
@@ -0,0 +1,20 @@
import { computed, ref } from 'vue';
export function useCrudListState(initial) {
const query = ref({ ...initial, sorts: [...initial.sorts], filters: [...initial.filters] });
const selectedId = ref(null);
const dirty = ref(false);
function replace(next) { query.value = { ...next, sorts: [...next.sorts], filters: [...next.filters] }; }
function setPage(page, pageSize = query.value.pageSize) { query.value = { ...query.value, page, pageSize }; }
function setSearch(search) { query.value = { ...query.value, page: 1, search: search?.trim() || undefined }; }
function reset() { replace(initial); selectedId.value = null; dirty.value = false; }
return {
query,
selectedId,
dirty,
offset: computed(() => (query.value.page - 1) * query.value.pageSize),
replace,
setPage,
setSearch,
reset
};
}
@@ -0,0 +1,31 @@
import { ref } from 'vue';
import { createIdempotencyKey } from '../commands/idempotency';
export function useOptimisticCommand(execute) {
const pending = ref(false);
const conflict = ref(false);
const lastCorrelationId = ref();
async function run(request) {
if (pending.value)
throw new Error('Command is already in progress');
pending.value = true;
conflict.value = false;
try {
const headers = { 'Idempotency-Key': createIdempotencyKey() };
if (request.etag)
headers['If-Match'] = request.etag;
const response = await execute(request, headers);
lastCorrelationId.value = response.correlationId;
return response;
}
catch (error) {
const status = error?.response?.status;
if (status === 409 || status === 412)
conflict.value = true;
throw error;
}
finally {
pending.value = false;
}
}
return { pending, conflict, lastCorrelationId, run };
}