Migrate remaining legacy operator workflows
This commit is contained in:
@@ -13,9 +13,11 @@
|
||||
const MAXIMUM_QUERY_LENGTH = 64;
|
||||
const MAXIMUM_WEB_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
|
||||
const requestIdPattern = /^[A-Za-z0-9_-]{1,128}$/;
|
||||
const themeCodePattern = /^[0-9]{8}$/;
|
||||
const newThemeCodePattern = /^[0-9]{8}$/;
|
||||
const existingThemeCodePattern = /^[0-9]{3,8}$/;
|
||||
const expertCodePattern = /^[0-9]{4}$/;
|
||||
const stockCodePattern = /^[A-Za-z0-9]{1,32}$/;
|
||||
const stockMarkets = Object.freeze(["kospi", "kosdaq", "nxt-kospi", "nxt-kosdaq"]);
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
const unsafeTextPattern = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}\uFFFD]/u;
|
||||
|
||||
@@ -44,6 +46,12 @@
|
||||
})
|
||||
});
|
||||
|
||||
const stockMasterContract = Object.freeze({
|
||||
request: "search-stocks",
|
||||
result: "stock-search-results",
|
||||
error: "stock-search-error"
|
||||
});
|
||||
|
||||
function hasExactKeys(value, keys) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const actual = Object.keys(value).sort();
|
||||
@@ -95,7 +103,7 @@
|
||||
if (!hasExactKeys(value, ["market", "session", "themeCode", "themeTitle"])) return null;
|
||||
const market = normalizeMarket(value.market);
|
||||
if (!market || !normalizeSession(value.session, market) ||
|
||||
!themeCodePattern.test(value.themeCode) ||
|
||||
!existingThemeCodePattern.test(value.themeCode) ||
|
||||
!isSafeCanonicalText(value.themeTitle, 128) ||
|
||||
(market === "nxt" && /\(NXT\)/i.test(value.themeTitle))) return null;
|
||||
return Object.freeze({
|
||||
@@ -141,7 +149,7 @@
|
||||
function normalizeThemeDraft(value) {
|
||||
if (!hasExactKeys(value, ["market", "themeCode", "themeTitle", "items"])) return null;
|
||||
const market = normalizeMarket(value.market);
|
||||
if (!market || !themeCodePattern.test(value.themeCode) ||
|
||||
if (!market || !existingThemeCodePattern.test(value.themeCode) ||
|
||||
!isSafeCanonicalText(value.themeTitle, 128) ||
|
||||
(market === "nxt" && /\(NXT\)/i.test(value.themeTitle))) return null;
|
||||
const items = normalizeThemeItems(value.items, market);
|
||||
@@ -250,6 +258,75 @@
|
||||
return Object.freeze({ requestId: requireRequestId(requestId) });
|
||||
}
|
||||
|
||||
function createStockMasterSearchRequest(requestId, query, maximumResults = MAXIMUM_RESULTS) {
|
||||
requireRequestId(requestId);
|
||||
const normalizedQuery = normalizeQuery(query);
|
||||
if (!normalizedQuery) throw new TypeError("A non-empty stock-master search query is required.");
|
||||
if (!Number.isInteger(maximumResults) || maximumResults < 1 ||
|
||||
maximumResults > MAXIMUM_RESULTS) {
|
||||
throw new RangeError("Stock-master results must be limited from 1 through 500.");
|
||||
}
|
||||
return Object.freeze({ requestId, query: normalizedQuery, maximumResults });
|
||||
}
|
||||
|
||||
function normalizeStockMasterIdentity(value) {
|
||||
if (!hasExactKeys(value, ["market", "source", "name", "code"]) ||
|
||||
!stockMarkets.includes(value.market) || !stockCodePattern.test(value.code) ||
|
||||
!isSafeCanonicalText(value.name, 200)) return null;
|
||||
const expectedSource = value.market === "kospi" || value.market === "kosdaq"
|
||||
? "oracle"
|
||||
: "mariaDb";
|
||||
if (value.source !== expectedSource) return null;
|
||||
return Object.freeze({
|
||||
market: value.market,
|
||||
source: value.source,
|
||||
name: value.name,
|
||||
code: value.code
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStockMasterSearchResult(value, expectedRequest) {
|
||||
if (!hasExactKeys(value, [
|
||||
"requestId", "query", "retrievedAt", "totalRowCount", "truncated", "results"
|
||||
]) || !expectedRequest || value.requestId !== expectedRequest.requestId ||
|
||||
value.query !== expectedRequest.query || !requestIdPattern.test(value.requestId) ||
|
||||
normalizeQuery(value.query) !== value.query ||
|
||||
!isSafeCanonicalText(value.retrievedAt, 64) ||
|
||||
!Number.isFinite(Date.parse(value.retrievedAt)) ||
|
||||
!Number.isInteger(value.totalRowCount) || value.totalRowCount < 0 ||
|
||||
value.truncated !== false || !Array.isArray(value.results) ||
|
||||
value.results.length !== value.totalRowCount ||
|
||||
value.results.length > expectedRequest.maximumResults) return null;
|
||||
|
||||
const results = [];
|
||||
const identities = new Set();
|
||||
const names = new Set();
|
||||
for (const raw of value.results) {
|
||||
const item = normalizeStockMasterIdentity(raw);
|
||||
const identity = item && `${item.market}\u0000${item.source}\u0000${item.code}\u0000${item.name}`;
|
||||
if (!item || identities.has(identity) || names.has(item.name)) return null;
|
||||
identities.add(identity);
|
||||
names.add(item.name);
|
||||
results.push(item);
|
||||
}
|
||||
return Object.freeze({
|
||||
requestId: value.requestId,
|
||||
query: value.query,
|
||||
retrievedAt: value.retrievedAt,
|
||||
totalRowCount: value.totalRowCount,
|
||||
truncated: false,
|
||||
results: Object.freeze(results)
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeStockMasterSearchError(value, expectedRequest) {
|
||||
if (!hasExactKeys(value, ["requestId", "query", "message"]) || !expectedRequest ||
|
||||
value.requestId !== expectedRequest.requestId || value.query !== expectedRequest.query ||
|
||||
!requestIdPattern.test(value.requestId) || normalizeQuery(value.query) !== value.query ||
|
||||
!isSafeCanonicalText(value.message, 512)) return null;
|
||||
return Object.freeze({ ...value });
|
||||
}
|
||||
|
||||
function normalizeThemeCatalogSelection(value) {
|
||||
if (!hasExactKeys(value, ["market", "session", "themeCode", "themeTitle", "source"])) return null;
|
||||
const identity = normalizeThemeIdentity({
|
||||
@@ -392,7 +469,7 @@
|
||||
}
|
||||
|
||||
function createBlankThemeDraft(market, themeCode) {
|
||||
if (!normalizeMarket(market) || !themeCodePattern.test(themeCode)) {
|
||||
if (!normalizeMarket(market) || !newThemeCodePattern.test(themeCode)) {
|
||||
throw new TypeError("A typed market and suggested eight-digit theme code are required.");
|
||||
}
|
||||
return { market, themeCode, themeTitle: "", items: [] };
|
||||
@@ -427,7 +504,9 @@
|
||||
function createThemeCreateRequest(requestId, draftValue) {
|
||||
requireRequestId(requestId);
|
||||
const draft = cloneThemeDraft(draftValue);
|
||||
if (!draft) throw new TypeError("A complete theme draft is required.");
|
||||
if (!draft || !newThemeCodePattern.test(draft.themeCode)) {
|
||||
throw new TypeError("A complete theme draft with a new eight-digit code is required.");
|
||||
}
|
||||
return Object.freeze({ requestId, draft });
|
||||
}
|
||||
|
||||
@@ -550,7 +629,7 @@
|
||||
if (!hasExactKeys(value, [
|
||||
"requestId", "market", "themeCode", "canMutate", "writeQuarantined"
|
||||
]) || !requestIdPattern.test(value.requestId) || !normalizeMarket(value.market) ||
|
||||
!themeCodePattern.test(value.themeCode) || typeof value.canMutate !== "boolean" ||
|
||||
!newThemeCodePattern.test(value.themeCode) || typeof value.canMutate !== "boolean" ||
|
||||
typeof value.writeQuarantined !== "boolean" ||
|
||||
(value.writeQuarantined && value.canMutate)) return null;
|
||||
if (expectedRequest && (value.requestId !== expectedRequest.requestId ||
|
||||
@@ -591,7 +670,8 @@
|
||||
]) || !requestIdPattern.test(value.requestId) ||
|
||||
(value.entity !== "theme" && value.entity !== "expert") ||
|
||||
!mutationOperations.has(value.operation) ||
|
||||
!(value.entity === "theme" ? themeCodePattern : expertCodePattern).test(value.identityCode) ||
|
||||
!(value.entity === "theme" ? existingThemeCodePattern : expertCodePattern)
|
||||
.test(value.identityCode) ||
|
||||
!uuidPattern.test(value.operationId) || !isSafeCanonicalText(value.committedAt, 64) ||
|
||||
!Number.isFinite(Date.parse(value.committedAt)) || value.writeQuarantined !== false) return null;
|
||||
if (expected && ["requestId", "entity", "operation", "identityCode"]
|
||||
@@ -721,6 +801,7 @@
|
||||
|
||||
return {
|
||||
bridgeContract,
|
||||
stockMasterContract,
|
||||
limits: Object.freeze({
|
||||
maximumThemeItems: MAXIMUM_THEME_ITEMS,
|
||||
maximumRecommendations: MAXIMUM_RECOMMENDATIONS,
|
||||
@@ -736,6 +817,10 @@
|
||||
createSchemaPreflightRequest,
|
||||
createThemeNextCodeRequest,
|
||||
createExpertNextCodeRequest,
|
||||
createStockMasterSearchRequest,
|
||||
normalizeStockMasterIdentity,
|
||||
normalizeStockMasterSearchResult,
|
||||
normalizeStockMasterSearchError,
|
||||
normalizeThemeCatalogSelection,
|
||||
normalizeExpertCatalogSelection,
|
||||
createThemeEditDraft,
|
||||
|
||||
Reference in New Issue
Block a user