Update Extension files and add deploy script
This commit is contained in:
@@ -52,3 +52,9 @@
|
|||||||
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696cbd07000703cf5437","payload":{"account_platform_market":"DE","account_platform_account_id":"ihaveitmusic","account_shop_name":"iHaveit","account_sells":280535},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name","account_sells"]},"timestamp":1768741616428,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696cbd07000703cf5437","payload":{"account_platform_market":"DE","account_platform_account_id":"ihaveitmusic","account_shop_name":"iHaveit","account_sells":280535},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name","account_sells"]},"timestamp":1768741616428,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"ihaveitmusic","account_shop_name":"iHaveit","account_sells":280535}},"timestamp":1768741616427,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"ihaveitmusic","account_shop_name":"iHaveit","account_sells":280535}},"timestamp":1768741616427,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696cbd07000703cf5437"},"timestamp":1768741616513,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696cbd07000703cf5437"},"timestamp":1768741616513,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
|
{"location":"ebayParserService.js:150","message":"getExtensionId: not found after retries","data":{"hasWindow":true},"timestamp":1768742766564,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
||||||
|
{"location":"ebayParserService.js:150","message":"getExtensionId: not found after retries","data":{"hasWindow":true},"timestamp":1768742773836,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
||||||
|
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742796643,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
||||||
|
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742817118,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
||||||
|
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742900325,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
||||||
|
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742905657,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
||||||
|
|||||||
@@ -218,10 +218,34 @@ async function handleScanProductsRequest(url, accountId, sendResponse) {
|
|||||||
// Send parse message to content script
|
// Send parse message to content script
|
||||||
chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" })
|
chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response && response.ok && response.data) {
|
if (response && response.ok) {
|
||||||
handleScanComplete(tabId, response.data);
|
// Prüfe ob items vorhanden und nicht leer
|
||||||
|
const items = response.items || response.data?.items || [];
|
||||||
|
const meta = response.meta || response.data?.meta || {};
|
||||||
|
|
||||||
|
// Log meta für Debugging
|
||||||
|
console.log("[SCAN meta]", meta);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
// Leere items: behandele als Fehler mit meta
|
||||||
|
cleanupScanRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: "empty_items",
|
||||||
|
meta: meta
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Erfolg: sende items + meta
|
||||||
|
handleScanComplete(tabId, { items, meta });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cleanupScanRequest(tabId, null, { ok: false, error: response?.error || "Parsing failed" });
|
// Fehler: sende error + meta
|
||||||
|
const meta = response?.meta || {};
|
||||||
|
console.log("[SCAN meta]", meta);
|
||||||
|
cleanupScanRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: response?.error || "Parsing failed",
|
||||||
|
meta: meta
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -273,8 +297,10 @@ async function cleanupScanRequest(tabId, data, error) {
|
|||||||
// Send response
|
// Send response
|
||||||
if (request.sendResponse) {
|
if (request.sendResponse) {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
// error kann bereits meta enthalten
|
||||||
request.sendResponse(error);
|
request.sendResponse(error);
|
||||||
} else if (data) {
|
} else if (data) {
|
||||||
|
// data kann items + meta enthalten
|
||||||
request.sendResponse({ ok: true, data: data });
|
request.sendResponse({ ok: true, data: data });
|
||||||
} else {
|
} else {
|
||||||
request.sendResponse({ ok: false, error: "Unknown error" });
|
request.sendResponse({ ok: false, error: "Unknown error" });
|
||||||
|
|||||||
@@ -26,17 +26,39 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.action === "PARSE_PRODUCT_LIST") {
|
if (message.action === "PARSE_PRODUCT_LIST") {
|
||||||
try {
|
// async function, need to return promise
|
||||||
const items = parseProductList();
|
parseProductList()
|
||||||
sendResponse({ ok: true, data: { items } });
|
.then(result => {
|
||||||
} catch (error) {
|
// result hat bereits die Struktur { ok, items?, error?, meta }
|
||||||
// Niemals unhandled throws - immer graceful response
|
if (result.ok) {
|
||||||
console.error("Error parsing product list:", error);
|
sendResponse({
|
||||||
sendResponse({
|
ok: true,
|
||||||
ok: false,
|
items: result.items || [],
|
||||||
error: error.message || "Failed to parse product list"
|
meta: result.meta || {}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: result.error || "Failed to parse product list",
|
||||||
|
meta: result.meta || {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Niemals unhandled throws - immer graceful response
|
||||||
|
console.error("Error parsing product list:", error);
|
||||||
|
const pageType = detectPageType();
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Failed to parse product list",
|
||||||
|
meta: {
|
||||||
|
pageType,
|
||||||
|
finalUrl: window.location.href,
|
||||||
|
attempts: 1,
|
||||||
|
reason: "unhandled_error"
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
return true; // async response
|
return true; // async response
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -471,10 +493,259 @@ function parseEbayPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parst Produktliste von eBay Storefront oder Seller Listings
|
* Erkennt den Seitentyp basierend auf URL und DOM
|
||||||
* @returns {Array} Array von Produkt-Items
|
* @returns {string} Page Type: "storefront" | "seller_profile" | "feedback" | "search_results" | "unknown"
|
||||||
*/
|
*/
|
||||||
function parseProductList() {
|
function detectPageType() {
|
||||||
|
try {
|
||||||
|
const pathname = window.location.pathname.toLowerCase();
|
||||||
|
const search = window.location.search.toLowerCase();
|
||||||
|
|
||||||
|
// Storefront
|
||||||
|
if (pathname.includes("/str/")) {
|
||||||
|
return "storefront";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seller Profile
|
||||||
|
if (pathname.includes("/usr/")) {
|
||||||
|
return "seller_profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
if (search.includes("_tab=feedback") || pathname.includes("feedback")) {
|
||||||
|
return "feedback";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search Results
|
||||||
|
if (document.querySelector("ul.srp-results")) {
|
||||||
|
return "search_results";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
} catch (e) {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wartet auf Item-Links, bis minLinks gefunden wurden oder Timeout
|
||||||
|
* @param {number} maxMs - Maximale Wartezeit in ms (default: 4000)
|
||||||
|
* @param {number} intervalMs - Intervall zwischen Checks in ms (default: 250)
|
||||||
|
* @param {number} minLinks - Minimale Anzahl Links (default: 5)
|
||||||
|
* @returns {Promise<Array>} Array von Link-Elementen (kann 0 sein)
|
||||||
|
*/
|
||||||
|
async function waitForItemLinks(maxMs = 4000, intervalMs = 250, minLinks = 5) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxMs) {
|
||||||
|
const links = findItemLinks();
|
||||||
|
if (links.length >= minLinks) {
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warte auf nächsten Check
|
||||||
|
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout: gib gefundene Links zurück (kann 0 sein)
|
||||||
|
return findItemLinks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Findet Navigations-Link zu Items-Seite (Angebote/Artikel)
|
||||||
|
* @returns {string|null} Absolute URL oder null
|
||||||
|
*/
|
||||||
|
function findItemsNavigationLink() {
|
||||||
|
try {
|
||||||
|
const allLinks = Array.from(document.querySelectorAll("a"));
|
||||||
|
const candidates = [];
|
||||||
|
|
||||||
|
for (const link of allLinks) {
|
||||||
|
const href = link.href || link.getAttribute("href");
|
||||||
|
const text = (link.textContent || "").toLowerCase().trim();
|
||||||
|
|
||||||
|
if (!href) continue;
|
||||||
|
|
||||||
|
const hrefLower = href.toLowerCase();
|
||||||
|
|
||||||
|
// Prüfe Anchor-Text
|
||||||
|
const textMatches =
|
||||||
|
text.includes("artikel") ||
|
||||||
|
text.includes("angebote") ||
|
||||||
|
text.includes("items") ||
|
||||||
|
text.includes("items for sale") ||
|
||||||
|
text.includes("shop") ||
|
||||||
|
text.includes("shop anzeigen");
|
||||||
|
|
||||||
|
// Prüfe href-Patterns
|
||||||
|
const hrefMatches =
|
||||||
|
hrefLower.includes("/sch/") ||
|
||||||
|
hrefLower.includes("/s/i.html") ||
|
||||||
|
hrefLower.includes("/str/") ||
|
||||||
|
hrefLower.includes("?_tab=selling");
|
||||||
|
|
||||||
|
if (textMatches || hrefMatches) {
|
||||||
|
// Berechne Priorität
|
||||||
|
let priority = 0;
|
||||||
|
if (hrefLower.includes("/sch/")) priority = 1;
|
||||||
|
else if (hrefLower.includes("/str/")) priority = 2;
|
||||||
|
else if (hrefLower.includes("/s/i.html")) priority = 3;
|
||||||
|
else if (text.includes("items")) priority = 4;
|
||||||
|
else priority = 5;
|
||||||
|
|
||||||
|
candidates.push({ link, href, priority });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortiere nach Priorität (niedrigste Zahl = höchste Priorität)
|
||||||
|
candidates.sort((a, b) => a.priority - b.priority);
|
||||||
|
|
||||||
|
const bestCandidate = candidates[0].href;
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
|
||||||
|
// Prüfe ob URL sich unterscheidet
|
||||||
|
if (bestCandidate === currentUrl) {
|
||||||
|
return null; // Gleiche URL, keine Navigation nötig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erstelle absolute URL falls nötig
|
||||||
|
try {
|
||||||
|
const url = new URL(bestCandidate, window.location.origin);
|
||||||
|
return url.href;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst Produktliste von eBay Storefront oder Seller Listings
|
||||||
|
* @returns {Promise<{ok: boolean, items?: Array, error?: string, meta: object}>}
|
||||||
|
*/
|
||||||
|
async function parseProductList() {
|
||||||
|
let attempts = 1;
|
||||||
|
let pageType = detectPageType();
|
||||||
|
let finalUrl = window.location.href;
|
||||||
|
let reason = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Schritt 1: Warte auf Item-Links (Polling)
|
||||||
|
let links = await waitForItemLinks(4000, 250, 5);
|
||||||
|
|
||||||
|
// Schritt 2: Wenn keine Links gefunden, versuche Auto-Navigation (max. 1 Retry)
|
||||||
|
if (links.length === 0 && attempts === 1) {
|
||||||
|
const itemsLink = findItemsNavigationLink();
|
||||||
|
|
||||||
|
if (itemsLink && itemsLink !== window.location.href) {
|
||||||
|
// Navigiere zur Items-Seite
|
||||||
|
window.location.href = itemsLink;
|
||||||
|
|
||||||
|
// Warte auf DOM-Ready nach Navigation
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1200));
|
||||||
|
|
||||||
|
// Warte erneut auf Item-Links (längeres Timeout nach Navigation)
|
||||||
|
links = await waitForItemLinks(5000, 250, 5);
|
||||||
|
|
||||||
|
attempts = 2;
|
||||||
|
pageType = detectPageType();
|
||||||
|
finalUrl = window.location.href;
|
||||||
|
reason = "navigated_to_items_page";
|
||||||
|
|
||||||
|
// Wenn immer noch keine Links, gib Fehler zurück
|
||||||
|
if (links.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "no_items_found",
|
||||||
|
meta: {
|
||||||
|
pageType,
|
||||||
|
finalUrl,
|
||||||
|
attempts,
|
||||||
|
reason: "no_items_found_after_navigation"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Kein Navigations-Link gefunden
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "no_items_found",
|
||||||
|
meta: {
|
||||||
|
pageType,
|
||||||
|
finalUrl,
|
||||||
|
attempts,
|
||||||
|
reason: links.length === 0 ? "no_items_links_on_page" : "timed_out_waiting_for_items"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schritt 3: Parse Items
|
||||||
|
if (links.length === 0) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "no_items_found",
|
||||||
|
meta: {
|
||||||
|
pageType,
|
||||||
|
finalUrl,
|
||||||
|
attempts,
|
||||||
|
reason: reason || "no_items_links_on_page"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each item
|
||||||
|
const parsedItems = [];
|
||||||
|
const seenIds = new Set();
|
||||||
|
|
||||||
|
for (const itemLink of links) {
|
||||||
|
try {
|
||||||
|
const item = parseItemFromLink(itemLink);
|
||||||
|
|
||||||
|
// Deduplicate by platformProductId
|
||||||
|
if (item.platformProductId && !seenIds.has(item.platformProductId)) {
|
||||||
|
seenIds.add(item.platformProductId);
|
||||||
|
parsedItems.push(item);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue with next item if one fails
|
||||||
|
console.warn("Failed to parse item:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return max 60 items (first page)
|
||||||
|
const result = parsedItems.slice(0, 60);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
items: result,
|
||||||
|
meta: {
|
||||||
|
pageType,
|
||||||
|
finalUrl,
|
||||||
|
attempts,
|
||||||
|
reason: reason || (result.length > 0 ? "items_found" : "parsed_zero_items")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Graceful error handling
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Failed to parse product list",
|
||||||
|
meta: {
|
||||||
|
pageType,
|
||||||
|
finalUrl,
|
||||||
|
attempts,
|
||||||
|
reason: "parse_error"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
const urlLower = url.toLowerCase();
|
const urlLower = url.toLowerCase();
|
||||||
|
|||||||
345
setup/deploy-upright-schema.ps1
Normal file
345
setup/deploy-upright-schema.ps1
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
<#
|
||||||
|
Deploy Upright DB Schema for Appwrite 1.8.1 (TablesDB)
|
||||||
|
- Writes appwrite.config.json
|
||||||
|
- Runs: appwrite push tables --force
|
||||||
|
|
||||||
|
Usage example:
|
||||||
|
.\deploy-upright-schema.ps1 `
|
||||||
|
-ProjectId "YOUR_PROJECT_ID" `
|
||||||
|
-Endpoint "https://YOUR_HOST_OR_REGION/v1" `
|
||||||
|
-DatabaseId "upright" `
|
||||||
|
-DatabaseName "upright"
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$ProjectId,
|
||||||
|
[Parameter(Mandatory=$true)][string]$Endpoint,
|
||||||
|
[Parameter(Mandatory=$true)][string]$DatabaseId,
|
||||||
|
[Parameter(Mandatory=$false)][string]$DatabaseName = "upright"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function New-IndexObject {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[Parameter(Mandatory=$true)][ValidateSet("key","unique","fulltext")][string]$Type,
|
||||||
|
[Parameter(Mandatory=$true)][string[]]$Attributes
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = $Type
|
||||||
|
attributes = $Attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnString {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[int]$Size = 255,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "string"
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
size = $Size
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnUrl {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[int]$Size = 2048,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
# In config, url is usually type "string" with format "url"
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "string"
|
||||||
|
format = "url"
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
size = $Size
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnDatetime {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "datetime"
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnBoolean {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "boolean"
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnInteger {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "integer"
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnFloat {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "float"
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-ColumnEnumString {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][string]$Key,
|
||||||
|
[Parameter(Mandatory=$true)][string[]]$Elements,
|
||||||
|
[int]$Size = 50,
|
||||||
|
[bool]$Required = $false,
|
||||||
|
[bool]$Array = $false,
|
||||||
|
$Default = $null
|
||||||
|
)
|
||||||
|
# Enum in Appwrite config is typically string with format "enum" and "elements"
|
||||||
|
return @{
|
||||||
|
key = $Key
|
||||||
|
type = "string"
|
||||||
|
format = "enum"
|
||||||
|
elements = $Elements
|
||||||
|
status = "available"
|
||||||
|
error = ""
|
||||||
|
required = $Required
|
||||||
|
array = $Array
|
||||||
|
size = $Size
|
||||||
|
default = $Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tables
|
||||||
|
$usersTableId = "users"
|
||||||
|
$accountsTableId = "accounts"
|
||||||
|
$productsTableId = "products"
|
||||||
|
$productDetailsTableId = "product_details"
|
||||||
|
|
||||||
|
$tables = @()
|
||||||
|
|
||||||
|
# users
|
||||||
|
$tables += @{
|
||||||
|
'$id' = $usersTableId
|
||||||
|
'$permissions' = @(
|
||||||
|
'create("any")','read("any")','update("any")','delete("any")'
|
||||||
|
)
|
||||||
|
databaseId = $DatabaseId
|
||||||
|
name = "users"
|
||||||
|
enabled = $true
|
||||||
|
rowSecurity = $false
|
||||||
|
columns = @(
|
||||||
|
New-ColumnDatetime -Key "user_created_at"
|
||||||
|
New-ColumnDatetime -Key "user_updated_at"
|
||||||
|
)
|
||||||
|
indexes = @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# accounts
|
||||||
|
$tables += @{
|
||||||
|
'$id' = $accountsTableId
|
||||||
|
'$permissions' = @(
|
||||||
|
'create("any")','read("any")','update("any")','delete("any")'
|
||||||
|
)
|
||||||
|
databaseId = $DatabaseId
|
||||||
|
name = "accounts"
|
||||||
|
enabled = $true
|
||||||
|
rowSecurity = $false
|
||||||
|
columns = @(
|
||||||
|
New-ColumnString -Key "account_owner_user_id" -Size 128 -Required $false
|
||||||
|
New-ColumnBoolean -Key "account_team" -Required $true -Default $false
|
||||||
|
|
||||||
|
New-ColumnEnumString -Key "account_platform" -Elements @("amazon","ebay") -Size 20 -Required $true
|
||||||
|
New-ColumnString -Key "account_platform_account_id" -Size 128 -Required $true
|
||||||
|
New-ColumnString -Key "account_platform_market" -Size 50 -Required $true
|
||||||
|
|
||||||
|
New-ColumnString -Key "account_shop_name" -Size 256 -Required $false
|
||||||
|
New-ColumnUrl -Key "account_url" -Size 2048 -Required $false
|
||||||
|
|
||||||
|
New-ColumnEnumString -Key "account_status" -Elements @("active","unknown","disabled") -Size 20 -Required $false
|
||||||
|
|
||||||
|
New-ColumnDatetime -Key "account_created_at"
|
||||||
|
New-ColumnDatetime -Key "account_updated_at"
|
||||||
|
)
|
||||||
|
indexes = @(
|
||||||
|
(New-IndexObject -Key "uniq_platform_market_accountid" -Type "unique" -Attributes @(
|
||||||
|
"account_platform","account_platform_market","account_platform_account_id"
|
||||||
|
)),
|
||||||
|
(New-IndexObject -Key "idx_owner_user" -Type "key" -Attributes @("account_owner_user_id")),
|
||||||
|
(New-IndexObject -Key "idx_team" -Type "key" -Attributes @("account_team"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# products (sparscan + vollscan in einem)
|
||||||
|
$tables += @{
|
||||||
|
'$id' = $productsTableId
|
||||||
|
'$permissions' = @(
|
||||||
|
'create("any")','read("any")','update("any")','delete("any")'
|
||||||
|
)
|
||||||
|
databaseId = $DatabaseId
|
||||||
|
name = "products"
|
||||||
|
enabled = $true
|
||||||
|
rowSecurity = $false
|
||||||
|
columns = @(
|
||||||
|
New-ColumnString -Key "product_account_id" -Size 36 -Required $true
|
||||||
|
|
||||||
|
New-ColumnEnumString -Key "product_platform" -Elements @("amazon","ebay") -Size 20 -Required $true
|
||||||
|
New-ColumnString -Key "product_platform_market" -Size 50 -Required $true
|
||||||
|
New-ColumnString -Key "product_platform_product_id" -Size 128 -Required $true
|
||||||
|
|
||||||
|
New-ColumnUrl -Key "product_url" -Size 2048 -Required $true
|
||||||
|
New-ColumnString -Key "product_title" -Size 512 -Required $false
|
||||||
|
|
||||||
|
New-ColumnEnumString -Key "product_condition" -Elements @(
|
||||||
|
"new","used_like_new","used_good","used_ok","parts","unknown"
|
||||||
|
) -Size 30 -Required $false -Default "unknown"
|
||||||
|
|
||||||
|
New-ColumnEnumString -Key "product_status" -Elements @(
|
||||||
|
"active","ended","unknown"
|
||||||
|
) -Size 20 -Required $false -Default "unknown"
|
||||||
|
|
||||||
|
# volatile (sparscan)
|
||||||
|
New-ColumnFloat -Key "product_price" -Required $false
|
||||||
|
New-ColumnString -Key "product_currency" -Size 10 -Required $false
|
||||||
|
New-ColumnInteger -Key "product_quantity_available" -Required $false
|
||||||
|
New-ColumnInteger -Key "product_quantity_sold" -Required $false
|
||||||
|
New-ColumnInteger -Key "product_watch_count" -Required $false
|
||||||
|
New-ColumnInteger -Key "product_in_carts_count" -Required $false
|
||||||
|
New-ColumnDatetime -Key "product_last_seen_at" -Required $false
|
||||||
|
|
||||||
|
# scan bookkeeping
|
||||||
|
New-ColumnDatetime -Key "product_first_fullscan_at" -Required $false
|
||||||
|
New-ColumnDatetime -Key "product_last_fullscan_at" -Required $false
|
||||||
|
New-ColumnDatetime -Key "product_last_sparscan_at" -Required $false
|
||||||
|
New-ColumnInteger -Key "product_details_version" -Required $false
|
||||||
|
)
|
||||||
|
indexes = @(
|
||||||
|
(New-IndexObject -Key "idx_product_account" -Type "key" -Attributes @("product_account_id")),
|
||||||
|
(New-IndexObject -Key "uniq_account_platform_product" -Type "unique" -Attributes @(
|
||||||
|
"product_account_id","product_platform_product_id"
|
||||||
|
)),
|
||||||
|
(New-IndexObject -Key "idx_platform_market" -Type "key" -Attributes @(
|
||||||
|
"product_platform","product_platform_market"
|
||||||
|
)),
|
||||||
|
(New-IndexObject -Key "idx_status" -Type "key" -Attributes @("product_status")),
|
||||||
|
(New-IndexObject -Key "ft_title" -Type "fulltext" -Attributes @("product_title"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# product_details (schwer)
|
||||||
|
# WICHTIG: Alle Attribute verwenden "product_detail_" Präfix (Singular) nicht "details_"
|
||||||
|
$tables += @{
|
||||||
|
'$id' = $productDetailsTableId
|
||||||
|
'$permissions' = @(
|
||||||
|
'create("any")','read("any")','update("any")','delete("any")'
|
||||||
|
)
|
||||||
|
databaseId = $DatabaseId
|
||||||
|
name = "product_details"
|
||||||
|
enabled = $true
|
||||||
|
rowSecurity = $false
|
||||||
|
columns = @(
|
||||||
|
New-ColumnString -Key "product_id" -Size 36 -Required $true
|
||||||
|
|
||||||
|
New-ColumnString -Key "product_detail_category_path" -Size 512 -Required $false
|
||||||
|
New-ColumnString -Key "product_detail_brand" -Size 128 -Required $false
|
||||||
|
New-ColumnString -Key "product_detail_mpn" -Size 128 -Required $false
|
||||||
|
New-ColumnString -Key "product_detail_gtin" -Size 128 -Required $false
|
||||||
|
|
||||||
|
New-ColumnString -Key "product_detail_description_text" -Size 20000 -Required $false
|
||||||
|
|
||||||
|
New-ColumnUrl -Key "product_detail_image_urls" -Size 2048 -Required $false -Array $true
|
||||||
|
|
||||||
|
New-ColumnString -Key "product_detail_item_specifics_json" -Size 20000 -Required $false
|
||||||
|
New-ColumnBoolean -Key "product_detail_has_variations" -Required $false
|
||||||
|
New-ColumnString -Key "product_detail_variations_json" -Size 20000 -Required $false
|
||||||
|
|
||||||
|
New-ColumnDatetime -Key "product_detail_last_updated_at" -Required $false
|
||||||
|
)
|
||||||
|
indexes = @(
|
||||||
|
(New-IndexObject -Key "uniq_product_id" -Type "unique" -Attributes @("product_id"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# appwrite.config.json root
|
||||||
|
$config = @{
|
||||||
|
projectId = $ProjectId
|
||||||
|
endpoint = $Endpoint
|
||||||
|
tablesDB = @(
|
||||||
|
@{
|
||||||
|
'$id' = $DatabaseId
|
||||||
|
name = $DatabaseName
|
||||||
|
enabled = $true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tables = $tables
|
||||||
|
}
|
||||||
|
|
||||||
|
$configPath = Join-Path -Path (Get-Location) -ChildPath "appwrite.config.json"
|
||||||
|
($config | ConvertTo-Json -Depth 20) | Set-Content -Path $configPath -Encoding UTF8
|
||||||
|
|
||||||
|
Write-Host "Wrote: $configPath"
|
||||||
|
Write-Host "Now pushing schema via: appwrite push tables --force"
|
||||||
|
|
||||||
|
# Push schema (requires: appwrite login, appwrite init project)
|
||||||
|
& appwrite push tables --force
|
||||||
Reference in New Issue
Block a user