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:
@@ -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 {};
|
||||
@@ -0,0 +1 @@
|
||||
export {};
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user