Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82efe19ae0 | ||
|
|
00eed86bb2 | ||
|
|
6edd46d32a | ||
|
|
340d26fe11 | ||
|
|
002d7146d0 | ||
|
|
e108e9da26 | ||
|
|
bff9e2bca4 | ||
|
|
8e949e419b | ||
|
|
f5e10c3579 | ||
|
|
1026aed80f | ||
|
|
9bf4da775c | ||
|
|
53bbf85fe1 | ||
|
|
fdc8aaea6a |
@@ -0,0 +1,63 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
--- event processing
|
||||||
|
|
||||||
|
DROP TRIGGER amazon_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_amazon_event;
|
||||||
|
|
||||||
|
DROP TRIGGER big_cartel_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_big_cartel_event;
|
||||||
|
|
||||||
|
DROP TRIGGER ebay_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_ebay_event;
|
||||||
|
|
||||||
|
DROP TRIGGER ecwid_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_ecwid_event;
|
||||||
|
|
||||||
|
DROP TRIGGER etsy_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_etsy_event;
|
||||||
|
|
||||||
|
DROP TRIGGER shopify_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_shopify_event;
|
||||||
|
|
||||||
|
DROP TRIGGER square_online_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_square_online_event;
|
||||||
|
|
||||||
|
DROP TRIGGER squarespace_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_squarespace_event;
|
||||||
|
|
||||||
|
DROP TRIGGER tiktok_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_tiktok_event;
|
||||||
|
|
||||||
|
DROP TRIGGER walmart_marketplace_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_walmart_marketplace_event;
|
||||||
|
|
||||||
|
DROP TRIGGER wix_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_wix_event;
|
||||||
|
|
||||||
|
DROP TRIGGER woo_commerce_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_woo_commerce_event;
|
||||||
|
|
||||||
|
DROP TRIGGER zoho_store_events ON mock.raw_shop_events;
|
||||||
|
DROP FUNCTION mock.process_raw_zoho_event;
|
||||||
|
|
||||||
|
-- event tables
|
||||||
|
|
||||||
|
DROP TABLE mock.shop_amazon_events;
|
||||||
|
DROP TABLE mock.shop_big_cartel_events;
|
||||||
|
DROP TABLE mock.shop_ebay_events;
|
||||||
|
DROP TABLE mock.shop_ecwid_events;
|
||||||
|
DROP TABLE mock.shop_etsy_events;
|
||||||
|
DROP TABLE mock.shop_shopify_events;
|
||||||
|
DROP TABLE mock.shop_square_online_events;
|
||||||
|
DROP TABLE mock.shop_squarespace_events;
|
||||||
|
DROP TABLE mock.shop_tiktok_events;
|
||||||
|
DROP TABLE mock.shop_walmart_marketplace_events;
|
||||||
|
DROP TABLE mock.shop_wix_events;
|
||||||
|
DROP TABLE mock.shop_woo_commerce_events;
|
||||||
|
DROP TABLE mock.shop_zoho_events;
|
||||||
|
DROP TABLE mock.raw_shop_events;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
-- raw shop events
|
||||||
|
|
||||||
|
CREATE TABLE mock.raw_shop_events (
|
||||||
|
platform TEXT NOT NULL,
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
raw_payload JSONB NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (platform, shop_id, event_id, event_timestamp)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- processed event tables
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_amazon_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'amazon' CHECK (platform = 'amazon'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_big_cartel_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'big_cartel' CHECK (platform = 'big_cartel'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_ebay_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'ebay' CHECK (platform = 'ebay'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_ecwid_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'ecwid' CHECK (platform = 'ecwid'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_etsy_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'etsy' CHECK (platform = 'etsy'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_shopify_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'shopify' CHECK (platform = 'shopify'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_square_online_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'square_online' CHECK (platform = 'square_online'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_squarespace_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'squarespace' CHECK (platform = 'squarespace'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_tiktok_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'tiktok' CHECK (platform = 'tiktok'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_walmart_marketplace_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'walmart_marketplace' CHECK (platform = 'walmart_marketplace'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_wix_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'wix' CHECK (platform = 'wix'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_woo_commerce_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'woo_commerce' CHECK (platform = 'woo_commerce'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.shop_zoho_events (
|
||||||
|
platform TEXT NOT NULL DEFAULT 'zoho' CHECK (platform = 'zoho'),
|
||||||
|
shop_id TEXT NOT NULL,
|
||||||
|
event_timestamp TIMESTAMPTZ NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (shop_id, event_id, event_timestamp),
|
||||||
|
FOREIGN KEY (platform, shop_id, event_id, event_timestamp) REFERENCES mock.raw_shop_events
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--- event processing
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_amazon_event() RETURNS TRIGGER AS $process_raw_amazon_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_amazon_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_amazon_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER amazon_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'amazon')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_amazon_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_big_cartel_event() RETURNS TRIGGER AS $process_raw_big_cartel_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_big_cartel_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_big_cartel_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER big_cartel_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'big_cartel')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_big_cartel_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_ebay_event() RETURNS TRIGGER AS $process_raw_ebay_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_ebay_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_ebay_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER ebay_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'ebay')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_ebay_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_ecwid_event() RETURNS TRIGGER AS $process_raw_ecwid_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_ecwid_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_ecwid_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER ecwid_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'ecwid')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_ecwid_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_etsy_event() RETURNS TRIGGER AS $process_raw_etsy_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_etsy_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_etsy_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER etsy_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'etsy')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_etsy_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_shopify_event() RETURNS TRIGGER AS $process_raw_shopify_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_shopify_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_shopify_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER shopify_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'shopify')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_shopify_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_square_online_event() RETURNS TRIGGER AS $process_raw_square_online_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_square_online_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_square_online_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER square_online_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'square_online')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_square_online_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_squarespace_event() RETURNS TRIGGER AS $process_raw_squarespace_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_squarespace_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_squarespace_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER squarespace_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'squarespace')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_squarespace_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_tiktok_event() RETURNS TRIGGER AS $process_raw_tiktok_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_tiktok_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_tiktok_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER tiktok_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'tiktok')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_tiktok_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_walmart_marketplace_event() RETURNS TRIGGER AS $process_raw_walmart_marketplace_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_walmart_marketplace_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_walmart_marketplace_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER walmart_marketplace_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'walmart_marketplace')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_walmart_marketplace_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_wix_event() RETURNS TRIGGER AS $process_raw_wix_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_wix_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_wix_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER wix_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'wix')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_wix_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_woo_commerce_event() RETURNS TRIGGER AS $process_raw_woo_commerce_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_woo_commerce_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_woo_commerce_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER woo_commerce_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'woo_commerce')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_woo_commerce_event();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_zoho_event() RETURNS TRIGGER AS $process_raw_zoho_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_zoho_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_zoho_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER zoho_store_events
|
||||||
|
AFTER INSERT ON mock.raw_shop_events
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (NEW.platform = 'zoho')
|
||||||
|
EXECUTE FUNCTION mock.process_raw_zoho_event();
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
DROP TABLE mock.sale_form_shop_selection;
|
||||||
|
DROP TABLE mock.refund_form_shop_selection;
|
||||||
|
DROP TABLE mock.count_form_shop_selection;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE mock.sale_form_shop_selection (
|
||||||
|
account_id INTEGER PRIMARY KEY,
|
||||||
|
platform PLATFORM NOT NULL,
|
||||||
|
shop_id TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.refund_form_shop_selection (
|
||||||
|
account_id INTEGER PRIMARY KEY,
|
||||||
|
platform PLATFORM NOT NULL,
|
||||||
|
shop_id TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE mock.count_form_shop_selection (
|
||||||
|
account_id INTEGER PRIMARY KEY,
|
||||||
|
platform PLATFORM NOT NULL,
|
||||||
|
shop_id TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_amazon_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_amazon_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_big_cartel_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_big_cartel_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_ebay_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_ebay_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_ecwid_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_ecwid_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_etsy_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_etsy_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_shopify_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_shopify_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_square_online_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_square_online_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_squarespace_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_squarespace_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_tiktok_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_tiktok_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_walmart_marketplace_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_walmart_marketplace_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_wix_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_wix_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_woo_commerce_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_woo_commerce_events_by_timestamp;
|
||||||
|
ALTER TABLE mock.shop_zoho_events
|
||||||
|
DROP COLUMN processed,
|
||||||
|
DROP COLUMN processed_successfully;
|
||||||
|
DROP INDEX mock.shop_zoho_events_by_timestamp;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_amazon_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_amazon_events_by_timestamp ON mock.shop_amazon_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_amazon_events_by_timestamp_unprocessed ON mock.shop_amazon_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_big_cartel_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_big_cartel_events_by_timestamp ON mock.shop_big_cartel_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_big_cartel_events_by_timestamp_unprocessed ON mock.shop_big_cartel_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_ebay_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_ebay_events_by_timestamp ON mock.shop_ebay_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_ebay_events_by_timestamp_unprocessed ON mock.shop_ebay_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_ecwid_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_ecwid_events_by_timestamp ON mock.shop_ecwid_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_ecwid_events_by_timestamp_unprocessed ON mock.shop_ecwid_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_etsy_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_etsy_events_by_timestamp ON mock.shop_etsy_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_etsy_events_by_timestamp_unprocessed ON mock.shop_etsy_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_shopify_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_shopify_events_by_timestamp ON mock.shop_shopify_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_shopify_events_by_timestamp_unprocessed ON mock.shop_shopify_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_square_online_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_square_online_events_by_timestamp ON mock.shop_square_online_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_square_online_events_by_timestamp_unprocessed ON mock.shop_square_online_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_squarespace_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_squarespace_events_by_timestamp ON mock.shop_squarespace_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_squarespace_events_by_timestamp_unprocessed ON mock.shop_squarespace_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_tiktok_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_tiktok_events_by_timestamp ON mock.shop_tiktok_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_tiktok_events_by_timestamp_unprocessed ON mock.shop_tiktok_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_walmart_marketplace_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_walmart_marketplace_events_by_timestamp ON mock.shop_walmart_marketplace_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_walmart_marketplace_events_by_timestamp_unprocessed ON mock.shop_walmart_marketplace_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_wix_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_wix_events_by_timestamp ON mock.shop_wix_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_wix_events_by_timestamp_unprocessed ON mock.shop_wix_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_woo_commerce_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_woo_commerce_events_by_timestamp ON mock.shop_woo_commerce_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_woo_commerce_events_by_timestamp_unprocessed ON mock.shop_woo_commerce_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
ALTER TABLE mock.shop_zoho_events
|
||||||
|
ADD COLUMN processed BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN processed_successfully BOOLEAN DEFAULT FALSE;
|
||||||
|
CREATE INDEX shop_zoho_events_by_timestamp ON mock.shop_zoho_events (shop_id, event_timestamp);
|
||||||
|
CREATE INDEX shop_zoho_events_by_timestamp_unprocessed ON mock.shop_zoho_events (shop_id, event_timestamp) WHERE NOT processed;
|
||||||
|
|
||||||
|
|
||||||
|
--- event processing
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_amazon_event() RETURNS TRIGGER AS $process_raw_amazon_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_amazon_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_amazon_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_amazon_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_big_cartel_event() RETURNS TRIGGER AS $process_raw_big_cartel_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_big_cartel_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_big_cartel_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_big_cartel_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_ebay_event() RETURNS TRIGGER AS $process_raw_ebay_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_ebay_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_ebay_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_ebay_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_ecwid_event() RETURNS TRIGGER AS $process_raw_ecwid_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_ecwid_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_ecwid_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_ecwid_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_etsy_event() RETURNS TRIGGER AS $process_raw_etsy_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_etsy_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_etsy_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_etsy_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_shopify_event() RETURNS TRIGGER AS $process_raw_shopify_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_shopify_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_shopify_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_shopify_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_square_online_event() RETURNS TRIGGER AS $process_raw_square_online_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_square_online_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_square_online_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_square_online_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_squarespace_event() RETURNS TRIGGER AS $process_raw_squarespace_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_squarespace_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_squarespace_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_squarespace_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_tiktok_event() RETURNS TRIGGER AS $process_raw_tiktok_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_tiktok_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_tiktok_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_tiktok_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_walmart_marketplace_event() RETURNS TRIGGER AS $process_raw_walmart_marketplace_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_walmart_marketplace_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_walmart_marketplace_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_walmart_marketplace_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_wix_event() RETURNS TRIGGER AS $process_raw_wix_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_wix_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_wix_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_wix_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_woo_commerce_event() RETURNS TRIGGER AS $process_raw_woo_commerce_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_woo_commerce_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_woo_commerce_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_woo_commerce_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION mock.process_raw_zoho_event() RETURNS TRIGGER AS $process_raw_zoho_event$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO mock.shop_zoho_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
NEW.platform,
|
||||||
|
NEW.shop_id,
|
||||||
|
NEW.event_timestamp,
|
||||||
|
NEW.event_id;
|
||||||
|
|
||||||
|
PERFORM pg_notify('mock_shop_zoho_event_inserted', null);
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
END;
|
||||||
|
$process_raw_zoho_event$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
|
||||||
|
DROP VIEW mock.shop_amazon_listing_counts;
|
||||||
|
DROP VIEW mock.shop_amazon_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_big_cartel_listing_counts;
|
||||||
|
DROP VIEW mock.shop_big_cartel_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_ebay_listing_counts;
|
||||||
|
DROP VIEW mock.shop_ebay_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_ecwid_listing_counts;
|
||||||
|
DROP VIEW mock.shop_ecwid_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_etsy_listing_counts;
|
||||||
|
DROP VIEW mock.shop_etsy_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_shopify_listing_counts;
|
||||||
|
DROP VIEW mock.shop_shopify_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_square_online_listing_counts;
|
||||||
|
DROP VIEW mock.shop_square_online_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_squarespace_listing_counts;
|
||||||
|
DROP VIEW mock.shop_squarespace_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_tiktok_listing_counts;
|
||||||
|
DROP VIEW mock.shop_tiktok_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_walmart_marketplace_listing_counts;
|
||||||
|
DROP VIEW mock.shop_walmart_marketplace_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_wix_listing_counts;
|
||||||
|
DROP VIEW mock.shop_wix_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_woo_commerce_listing_counts;
|
||||||
|
DROP VIEW mock.shop_woo_commerce_listing_event_sequence;
|
||||||
|
DROP VIEW mock.shop_zoho_listing_counts;
|
||||||
|
DROP VIEW mock.shop_zoho_listing_event_sequence;
|
||||||
|
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 471 KiB |
@@ -9,6 +9,16 @@ entity "**accounts**" {
|
|||||||
*""user_id"": //text [FK]//
|
*""user_id"": //text [FK]//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**raw_shop_events**" {
|
||||||
|
+ ""platform"": //text [PK]//
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""raw_payload"": //jsonb //
|
||||||
|
*""parsed"": //boolean //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_amazon**" {
|
entity "**shop_amazon**" {
|
||||||
+ ""account_id"": //integer [PK][FK]//
|
+ ""account_id"": //integer [PK][FK]//
|
||||||
+ ""shop_id"": //text [PK]//
|
+ ""shop_id"": //text [PK]//
|
||||||
@@ -17,6 +27,14 @@ entity "**shop_amazon**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_amazon_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_amazon_listings**" {
|
entity "**shop_amazon_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -63,6 +81,14 @@ entity "**shop_big_cartel**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_big_cartel_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_big_cartel_listings**" {
|
entity "**shop_big_cartel_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -109,6 +135,14 @@ entity "**shop_ebay**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_ebay_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_ebay_listings**" {
|
entity "**shop_ebay_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -155,6 +189,14 @@ entity "**shop_ecwid**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_ecwid_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_ecwid_listings**" {
|
entity "**shop_ecwid_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -201,6 +243,14 @@ entity "**shop_etsy**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_etsy_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_etsy_listings**" {
|
entity "**shop_etsy_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -247,6 +297,14 @@ entity "**shop_shopify**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_shopify_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_shopify_listings**" {
|
entity "**shop_shopify_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -293,6 +351,14 @@ entity "**shop_square_online**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_square_online_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_square_online_listings**" {
|
entity "**shop_square_online_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -339,6 +405,14 @@ entity "**shop_squarespace**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_squarespace_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_squarespace_listings**" {
|
entity "**shop_squarespace_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -385,6 +459,14 @@ entity "**shop_tiktok**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_tiktok_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_tiktok_listings**" {
|
entity "**shop_tiktok_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -431,6 +513,14 @@ entity "**shop_walmart_marketplace**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_walmart_marketplace_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_walmart_marketplace_listings**" {
|
entity "**shop_walmart_marketplace_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -477,6 +567,14 @@ entity "**shop_wix**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_wix_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_wix_listings**" {
|
entity "**shop_wix_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -523,6 +621,14 @@ entity "**shop_woo_commerce**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_woo_commerce_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_woo_commerce_listings**" {
|
entity "**shop_woo_commerce_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -569,6 +675,14 @@ entity "**shop_zoho**" {
|
|||||||
*""name"": //text //
|
*""name"": //text //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**shop_zoho_events**" {
|
||||||
|
+ ""store_id"": //text [PK]//
|
||||||
|
+ ""event_timestamp"": //timestamp with time zone [PK][FK]//
|
||||||
|
+ ""event_id"": //text [PK]//
|
||||||
|
--
|
||||||
|
*""platform"": //text //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**shop_zoho_listings**" {
|
entity "**shop_zoho_listings**" {
|
||||||
+ ""shop_id"": //text [PK][FK]//
|
+ ""shop_id"": //text [PK][FK]//
|
||||||
+ ""listing_id"": //text [PK]//
|
+ ""listing_id"": //text [PK]//
|
||||||
@@ -654,6 +768,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_amazon**" }-- "**public.oauth_users**"
|
"**shop_amazon**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_amazon_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_amazon_listings**" }-- "**accounts**"
|
"**shop_amazon_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_amazon_listings**" ||-|| "**shop_amazon**"
|
"**shop_amazon_listings**" ||-|| "**shop_amazon**"
|
||||||
@@ -684,6 +800,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_big_cartel**" }-- "**public.oauth_users**"
|
"**shop_big_cartel**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_big_cartel_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_big_cartel_listings**" }-- "**accounts**"
|
"**shop_big_cartel_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_big_cartel_listings**" ||-|| "**shop_big_cartel**"
|
"**shop_big_cartel_listings**" ||-|| "**shop_big_cartel**"
|
||||||
@@ -714,6 +832,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_ebay**" }-- "**public.oauth_users**"
|
"**shop_ebay**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_ebay_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_ebay_listings**" }-- "**accounts**"
|
"**shop_ebay_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_ebay_listings**" ||-|| "**shop_ebay**"
|
"**shop_ebay_listings**" ||-|| "**shop_ebay**"
|
||||||
@@ -744,6 +864,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_ecwid**" }-- "**public.oauth_users**"
|
"**shop_ecwid**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_ecwid_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_ecwid_listings**" }-- "**accounts**"
|
"**shop_ecwid_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_ecwid_listings**" ||-|| "**shop_ecwid**"
|
"**shop_ecwid_listings**" ||-|| "**shop_ecwid**"
|
||||||
@@ -774,6 +896,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_etsy**" }-- "**public.oauth_users**"
|
"**shop_etsy**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_etsy_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_etsy_listings**" }-- "**accounts**"
|
"**shop_etsy_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_etsy_listings**" ||-|| "**shop_etsy**"
|
"**shop_etsy_listings**" ||-|| "**shop_etsy**"
|
||||||
@@ -804,6 +928,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_shopify**" }-- "**public.oauth_users**"
|
"**shop_shopify**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_shopify_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_shopify_listings**" }-- "**accounts**"
|
"**shop_shopify_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_shopify_listings**" ||-|| "**shop_shopify**"
|
"**shop_shopify_listings**" ||-|| "**shop_shopify**"
|
||||||
@@ -834,6 +960,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_square_online**" }-- "**public.oauth_users**"
|
"**shop_square_online**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_square_online_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_square_online_listings**" }-- "**accounts**"
|
"**shop_square_online_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_square_online_listings**" ||-|| "**shop_square_online**"
|
"**shop_square_online_listings**" ||-|| "**shop_square_online**"
|
||||||
@@ -864,6 +992,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_squarespace**" }-- "**public.oauth_users**"
|
"**shop_squarespace**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_squarespace_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_squarespace_listings**" }-- "**accounts**"
|
"**shop_squarespace_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_squarespace_listings**" ||-|| "**shop_squarespace**"
|
"**shop_squarespace_listings**" ||-|| "**shop_squarespace**"
|
||||||
@@ -894,6 +1024,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_tiktok**" }-- "**public.oauth_users**"
|
"**shop_tiktok**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_tiktok_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_tiktok_listings**" }-- "**accounts**"
|
"**shop_tiktok_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_tiktok_listings**" ||-|| "**shop_tiktok**"
|
"**shop_tiktok_listings**" ||-|| "**shop_tiktok**"
|
||||||
@@ -924,6 +1056,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_walmart_marketplace**" }-- "**public.oauth_users**"
|
"**shop_walmart_marketplace**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_walmart_marketplace_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_walmart_marketplace_listings**" }-- "**accounts**"
|
"**shop_walmart_marketplace_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_walmart_marketplace_listings**" ||-|| "**shop_walmart_marketplace**"
|
"**shop_walmart_marketplace_listings**" ||-|| "**shop_walmart_marketplace**"
|
||||||
@@ -954,6 +1088,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_wix**" }-- "**public.oauth_users**"
|
"**shop_wix**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_wix_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_wix_listings**" }-- "**accounts**"
|
"**shop_wix_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_wix_listings**" ||-|| "**shop_wix**"
|
"**shop_wix_listings**" ||-|| "**shop_wix**"
|
||||||
@@ -984,6 +1120,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_woo_commerce**" }-- "**public.oauth_users**"
|
"**shop_woo_commerce**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_woo_commerce_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_woo_commerce_listings**" }-- "**accounts**"
|
"**shop_woo_commerce_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_woo_commerce_listings**" ||-|| "**shop_woo_commerce**"
|
"**shop_woo_commerce_listings**" ||-|| "**shop_woo_commerce**"
|
||||||
@@ -1014,6 +1152,8 @@ entity "**sync_groups**" {
|
|||||||
|
|
||||||
"**shop_zoho**" }-- "**public.oauth_users**"
|
"**shop_zoho**" }-- "**public.oauth_users**"
|
||||||
|
|
||||||
|
"**shop_zoho_events**" ||-|| "**raw_shop_events**"
|
||||||
|
|
||||||
"**shop_zoho_listings**" }-- "**accounts**"
|
"**shop_zoho_listings**" }-- "**accounts**"
|
||||||
|
|
||||||
"**shop_zoho_listings**" ||-|| "**shop_zoho**"
|
"**shop_zoho_listings**" ||-|| "**shop_zoho**"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
@@ -51,6 +51,12 @@ entity "**etsy_users**" {
|
|||||||
*""shop_id"": //integer //
|
*""shop_id"": //integer //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity "**mock_mode**" {
|
||||||
|
+ ""account_id"": //integer [PK][FK]//
|
||||||
|
--
|
||||||
|
*""mock_mode"": //boolean //
|
||||||
|
}
|
||||||
|
|
||||||
entity "**oauth_login_states**" {
|
entity "**oauth_login_states**" {
|
||||||
+ ""state"": //bytea [PK]//
|
+ ""state"": //bytea [PK]//
|
||||||
--
|
--
|
||||||
@@ -149,6 +155,8 @@ entity "**wix_store_events**" {
|
|||||||
|
|
||||||
"**etsy_users**" }-- "**accounts**"
|
"**etsy_users**" }-- "**accounts**"
|
||||||
|
|
||||||
|
"**mock_mode**" ||-|| "**accounts**"
|
||||||
|
|
||||||
"**oauth_tokens**" }-- "**oauth_users**"
|
"**oauth_tokens**" }-- "**oauth_users**"
|
||||||
|
|
||||||
"**sync_group_listing_drafts**" }-- "**accounts**"
|
"**sync_group_listing_drafts**" }-- "**accounts**"
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ package accounts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
@@ -225,6 +227,44 @@ func (db *Store) GetAccountByEmail(ctx context.Context, email string) (Account,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Store) GetAccountIDByMockPlatformAndShopID(ctx context.Context, platform Platform, shopID string) (int64, error) {
|
||||||
|
info, ok := getMockShopSchemaInfo(platform)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("unrecognized platform: %w", consts.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.db.Query(
|
||||||
|
ctx,
|
||||||
|
fmt.Sprintf(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
account_id
|
||||||
|
FROM
|
||||||
|
%s
|
||||||
|
WHERE
|
||||||
|
shop_id = @shop_id
|
||||||
|
`,
|
||||||
|
info.shopTable,
|
||||||
|
),
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"shop_id": shopID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acctID, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int64])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return 0, consts.ErrNotFound
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("failed to scan rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acctID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (db *Store) GetUserAndAccountByAccessToken(ctx context.Context, accessToken string) (OAuthUser, *Account, error) {
|
func (db *Store) GetUserAndAccountByAccessToken(ctx context.Context, accessToken string) (OAuthUser, *Account, error) {
|
||||||
rows, err := db.db.Query(
|
rows, err := db.db.Query(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -1461,6 +1501,290 @@ func (db *Store) DeleteListingInListingInMockSyncGroupBeingEdited(ctx context.Co
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if err := s.accts.SetShopInMockSaleForm(c, acctID, platform, shopID); err != nil {
|
||||||
|
|
||||||
|
func (db *Store) SetShopInMockSaleForm(ctx context.Context, acctID int64, platform Platform, shopID string) error {
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO
|
||||||
|
mock.sale_form_shop_selection (
|
||||||
|
account_id,
|
||||||
|
platform,
|
||||||
|
shop_id
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@account_id,
|
||||||
|
@platform,
|
||||||
|
@shop_id
|
||||||
|
)
|
||||||
|
ON CONFLICT (account_id) DO UPDATE
|
||||||
|
SET platform = EXCLUDED.platform,
|
||||||
|
shop_id = EXCLUDED.shop_id
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"account_id": acctID,
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) SetShopInMockRefundForm(ctx context.Context, acctID int64, platform Platform, shopID string) error {
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO
|
||||||
|
mock.refund_form_shop_selection (
|
||||||
|
account_id,
|
||||||
|
platform,
|
||||||
|
shop_id
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@account_id,
|
||||||
|
@platform,
|
||||||
|
@shop_id
|
||||||
|
)
|
||||||
|
ON CONFLICT (account_id) DO UPDATE
|
||||||
|
SET platform = EXCLUDED.platform,
|
||||||
|
shop_id = EXCLUDED.shop_id
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"account_id": acctID,
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) SetShopInMockCountForm(ctx context.Context, acctID int64, platform Platform, shopID string) error {
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO
|
||||||
|
mock.count_form_shop_selection (
|
||||||
|
account_id,
|
||||||
|
platform,
|
||||||
|
shop_id
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@account_id,
|
||||||
|
@platform,
|
||||||
|
@shop_id
|
||||||
|
)
|
||||||
|
ON CONFLICT (account_id) DO UPDATE
|
||||||
|
SET platform = EXCLUDED.platform,
|
||||||
|
shop_id = EXCLUDED.shop_id
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"account_id": acctID,
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockEventFormValues struct {
|
||||||
|
SaleShopID string
|
||||||
|
SalePlatform Platform
|
||||||
|
RefundShopID string
|
||||||
|
RefundPlatform Platform
|
||||||
|
CountShopID string
|
||||||
|
CountPlatform Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) GetMockEventFormValues(ctx context.Context, acctID int64) (*MockEventFormValues, error) {
|
||||||
|
rows, err := db.db.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
sf.platform AS sale_platform,
|
||||||
|
sf.shop_id AS sale_shop_id,
|
||||||
|
rf.platform AS refund_platform,
|
||||||
|
rf.shop_id AS refund_shop_id,
|
||||||
|
cf.platform AS count_platform,
|
||||||
|
cf.shop_id AS count_shop_id
|
||||||
|
FROM
|
||||||
|
mock.sale_form_shop_selection AS sf
|
||||||
|
FULL OUTER JOIN
|
||||||
|
mock.refund_form_shop_selection AS rf
|
||||||
|
USING
|
||||||
|
(account_id)
|
||||||
|
FULL OUTER JOIN
|
||||||
|
mock.count_form_shop_selection AS cf
|
||||||
|
USING
|
||||||
|
(account_id)
|
||||||
|
WHERE
|
||||||
|
sf.account_id = @account_id
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"account_id": acctID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByNameLax[struct {
|
||||||
|
Sale_shop_id pgtype.Text
|
||||||
|
Sale_platform *Platform
|
||||||
|
Refund_shop_id pgtype.Text
|
||||||
|
Refund_platform *Platform
|
||||||
|
Count_shop_id pgtype.Text
|
||||||
|
Count_platform *Platform
|
||||||
|
}])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return &MockEventFormValues{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to scan rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MockEventFormValues{
|
||||||
|
SaleShopID: v.Sale_shop_id.String,
|
||||||
|
SalePlatform: deref(v.Sale_platform),
|
||||||
|
RefundShopID: v.Refund_shop_id.String,
|
||||||
|
RefundPlatform: deref(v.Refund_platform),
|
||||||
|
CountShopID: v.Count_shop_id.String,
|
||||||
|
CountPlatform: deref(v.Count_platform),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) SaveNewMockSale(ctx context.Context, acctID int64, platform Platform, shopID, listingID string, count int) (uuid.UUID, error) {
|
||||||
|
eventID := uuid.New()
|
||||||
|
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO
|
||||||
|
mock.raw_shop_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id,
|
||||||
|
raw_payload
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@platform,
|
||||||
|
@shop_id,
|
||||||
|
NOW(),
|
||||||
|
@event_id,
|
||||||
|
@raw_payload
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
"event_id": eventID,
|
||||||
|
"raw_payload": json.RawMessage(fmt.Sprintf(`{
|
||||||
|
"type": "sale",
|
||||||
|
"listingID": %q,
|
||||||
|
"count": %d
|
||||||
|
}`, listingID, count)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) SaveNewMockRefund(ctx context.Context, acctID int64, platform Platform, shopID, listingID string, count int) (uuid.UUID, error) {
|
||||||
|
eventID := uuid.New()
|
||||||
|
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO
|
||||||
|
mock.raw_shop_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id,
|
||||||
|
raw_payload
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@platform,
|
||||||
|
@shop_id,
|
||||||
|
NOW(),
|
||||||
|
@event_id,
|
||||||
|
@raw_payload
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
"event_id": eventID,
|
||||||
|
"raw_payload": json.RawMessage(fmt.Sprintf(`{
|
||||||
|
"type": "refund",
|
||||||
|
"listingID": %q,
|
||||||
|
"count": %d
|
||||||
|
}`, listingID, count)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) SaveNewMockInventoryReset(ctx context.Context, acctID int64, platform Platform, shopID, listingID string, count int) (uuid.UUID, error) {
|
||||||
|
eventID := uuid.New()
|
||||||
|
|
||||||
|
_, err := db.db.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
INSERT INTO
|
||||||
|
mock.raw_shop_events (
|
||||||
|
platform,
|
||||||
|
shop_id,
|
||||||
|
event_timestamp,
|
||||||
|
event_id,
|
||||||
|
raw_payload
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@platform,
|
||||||
|
@shop_id,
|
||||||
|
NOW(),
|
||||||
|
@event_id,
|
||||||
|
@raw_payload
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
"event_id": eventID,
|
||||||
|
"raw_payload": json.RawMessage(fmt.Sprintf(`{
|
||||||
|
"type": "inventory-reset",
|
||||||
|
"listingID": %q,
|
||||||
|
"count": %d
|
||||||
|
}`, listingID, count)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.Nil, fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// additional context
|
// additional context
|
||||||
|
|
||||||
func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) {
|
func (db *Store) GetAccountPointerByUserID(ctx context.Context, userID string) (*Account, error) {
|
||||||
@@ -1481,3 +1805,11 @@ func (db *Store) beginReadonlyTxn(ctx context.Context, cb func(pgx.Tx) error) er
|
|||||||
func (db *Store) beginTxn(ctx context.Context, cb func(pgx.Tx) error) error {
|
func (db *Store) beginTxn(ctx context.Context, cb func(pgx.Tx) error) error {
|
||||||
return pgx.BeginFunc(ctx, db.db, cb)
|
return pgx.BeginFunc(ctx, db.db, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deref[T any](ptr *T) T {
|
||||||
|
if ptr != nil {
|
||||||
|
return *ptr
|
||||||
|
}
|
||||||
|
var zero T
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,6 +105,22 @@ func (v_ctx *StoreWithContext) DeleteListingInListingInMockSyncGroupBeingEdited(
|
|||||||
return v_ctx.Store.DeleteListingInListingInMockSyncGroupBeingEdited(v_ctx.ctx, acctID, orderIndex)
|
return v_ctx.Store.DeleteListingInListingInMockSyncGroupBeingEdited(v_ctx.ctx, acctID, orderIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) SetShopInMockSaleForm(acctID int64, platform Platform, shopID string) error {
|
||||||
|
return v_ctx.Store.SetShopInMockSaleForm(v_ctx.ctx, acctID, platform, shopID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) SetShopInMockRefundForm(acctID int64, platform Platform, shopID string) error {
|
||||||
|
return v_ctx.Store.SetShopInMockRefundForm(v_ctx.ctx, acctID, platform, shopID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) SetShopInMockCountForm(acctID int64, platform Platform, shopID string) error {
|
||||||
|
return v_ctx.Store.SetShopInMockCountForm(v_ctx.ctx, acctID, platform, shopID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetMockEventFormValues(acctID int64) (*MockEventFormValues, error) {
|
||||||
|
return v_ctx.Store.GetMockEventFormValues(v_ctx.ctx, acctID)
|
||||||
|
}
|
||||||
|
|
||||||
func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) {
|
func (v_ctx *StoreWithContext) GetAccountPointerByUserID(userID string) (*Account, error) {
|
||||||
return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID)
|
return v_ctx.Store.GetAccountPointerByUserID(v_ctx.ctx, userID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1235,11 +1235,3 @@ func (db *Store) listMockSyncGroupListings(ctx context.Context, tx pgx.Tx, acctI
|
|||||||
|
|
||||||
return listings, nil
|
return listings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deref[T any](ptr *T) T {
|
|
||||||
if ptr == nil {
|
|
||||||
var zero T
|
|
||||||
return zero
|
|
||||||
}
|
|
||||||
return *ptr
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package amazon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"ruben/inventory2/domains/accounts"
|
||||||
|
"ruben/inventory2/domains/raw_events"
|
||||||
|
"ruben/inventory2/logging"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Mocks struct {
|
||||||
|
log *logging.Logger
|
||||||
|
db *pgxpool.Pool
|
||||||
|
listener MockEventListener
|
||||||
|
}
|
||||||
|
|
||||||
|
MockEventListener interface {
|
||||||
|
Notify(context.Context, raw_events.Event) error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventChannelName = "mock_shop_amazon_event_inserted"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMocks(log *logging.Logger, db *pgxpool.Pool) *Mocks {
|
||||||
|
return &Mocks{
|
||||||
|
log: log,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mocks) SetListener(l MockEventListener) *Mocks {
|
||||||
|
m.listener = l
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mocks) ProcessEvents(ctx context.Context) error {
|
||||||
|
notifCh, errCh := m.listenForNotifications(ctx)
|
||||||
|
|
||||||
|
for {
|
||||||
|
m.log.Debug("processing events")
|
||||||
|
if err := m.processUnprocessedEvents(ctx); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("error occurred while processing events: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case _, ok := <-notifCh:
|
||||||
|
if !ok {
|
||||||
|
return <-errCh
|
||||||
|
}
|
||||||
|
case <-time.After(time.Minute):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mocks) listenForNotifications(ctx context.Context) (<-chan struct{}, <-chan error) {
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
|
pc, err := m.db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("failed to acquire a connection: %w", err)
|
||||||
|
return nil, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := pc.Conn()
|
||||||
|
|
||||||
|
if _, err := conn.Exec(ctx, fmt.Sprintf("LISTEN %s", eventChannelName)); err != nil {
|
||||||
|
errCh <- fmt.Errorf("failed to start listening for notifications: %w", err)
|
||||||
|
return nil, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := make(chan struct{})
|
||||||
|
|
||||||
|
go func() (err error) {
|
||||||
|
defer func() {
|
||||||
|
pc.Release()
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
close(errCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, err := conn.WaitForNotification(ctx); err != nil {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("error occurred while waiting for the next notification: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch <- struct{}{}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mocks) processUnprocessedEvents(ctx context.Context) error {
|
||||||
|
for done := false; !done; {
|
||||||
|
err := pgx.BeginFunc(ctx, m.db, func(tx pgx.Tx) error {
|
||||||
|
rows, err := m.db.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
shop_id,
|
||||||
|
event_id,
|
||||||
|
event_timestamp
|
||||||
|
FROM
|
||||||
|
mock.shop_amazon_events
|
||||||
|
WHERE
|
||||||
|
NOT processed
|
||||||
|
ORDER BY
|
||||||
|
shop_id, event_timestamp
|
||||||
|
LIMIT
|
||||||
|
100
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evts, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
|
||||||
|
Shop_id string
|
||||||
|
Event_id string
|
||||||
|
Event_timestamp time.Time
|
||||||
|
}])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range evts {
|
||||||
|
if err := m.processEvent(ctx, tx, raw_events.Event{
|
||||||
|
Platform: string(accounts.Amazon),
|
||||||
|
StoreID: e.Shop_id,
|
||||||
|
EventID: e.Event_id,
|
||||||
|
EventTimestamp: e.Event_timestamp,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to process event: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if done = len(evts) == 0; !done {
|
||||||
|
m.log.Infof("processed %d events", len(evts))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mocks) processEvent(ctx context.Context, tx pgx.Tx, e raw_events.Event) error {
|
||||||
|
m.log.Debugf("processing (mock) event: %#v", e)
|
||||||
|
|
||||||
|
_, err := tx.Exec(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
UPDATE
|
||||||
|
mock.shop_amazon_events
|
||||||
|
SET
|
||||||
|
processed = true,
|
||||||
|
processed_successfully = true
|
||||||
|
WHERE
|
||||||
|
shop_id = @shop_id
|
||||||
|
AND event_id = @event_id
|
||||||
|
AND event_timestamp = @event_timestamp
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"shop_id": e.StoreID,
|
||||||
|
"event_id": e.EventID,
|
||||||
|
"event_timestamp": e.EventTimestamp,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.listener != nil {
|
||||||
|
go func() {
|
||||||
|
if err := m.listener.Notify(ctx, e); err != nil {
|
||||||
|
m.log.Errorf("error incurred by event listener: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package reports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"ruben/inventory2/consts"
|
||||||
|
"ruben/inventory2/domains/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
RawShopEvent struct {
|
||||||
|
Platform accounts.Platform
|
||||||
|
ShopID string
|
||||||
|
EventTimestamp time.Time
|
||||||
|
EventID string
|
||||||
|
RawPayload json.RawMessage
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *Store) GetRawShopEvents(ctx context.Context, acctID int64, platform accounts.Platform, shopID string) ([]RawShopEvent, error) {
|
||||||
|
if _, err := db.accts.GetMockShop(ctx, acctID, platform, shopID); err != nil {
|
||||||
|
if errors.Is(err, consts.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("shop not found: %w", err)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to look up shop: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.db.Query(
|
||||||
|
ctx,
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
event_timestamp,
|
||||||
|
event_id,
|
||||||
|
raw_payload
|
||||||
|
FROM
|
||||||
|
mock.raw_shop_events
|
||||||
|
WHERE
|
||||||
|
platform = @platform
|
||||||
|
AND shop_id = @shop_id
|
||||||
|
ORDER BY
|
||||||
|
event_timestamp DESC,
|
||||||
|
event_id ASC
|
||||||
|
`,
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"platform": platform,
|
||||||
|
"shop_id": shopID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vs, err := pgx.CollectRows(rows, pgx.RowToStructByNameLax[struct {
|
||||||
|
Event_timestamp time.Time
|
||||||
|
Event_id string
|
||||||
|
Raw_payload json.RawMessage
|
||||||
|
}])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
evts := make([]RawShopEvent, len(vs))
|
||||||
|
for i, v := range vs {
|
||||||
|
evts[i] = RawShopEvent{
|
||||||
|
Platform: platform,
|
||||||
|
ShopID: shopID,
|
||||||
|
EventTimestamp: v.Event_timestamp,
|
||||||
|
EventID: v.Event_id,
|
||||||
|
RawPayload: v.Raw_payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return evts, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package reports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
|
"ruben/inventory2/consts"
|
||||||
|
"ruben/inventory2/domains/accounts"
|
||||||
|
"ruben/inventory2/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate concurry -s Store
|
||||||
|
|
||||||
|
type (
|
||||||
|
Store struct {
|
||||||
|
log *logging.Logger
|
||||||
|
db *pgxpool.Pool
|
||||||
|
accts *accounts.Store
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewStore(logger *logging.Logger, db *pgxpool.Pool, accts *accounts.Store) *Store {
|
||||||
|
return &Store{
|
||||||
|
log: logger,
|
||||||
|
db: db,
|
||||||
|
accts: accts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) WithContext(ctx context.Context) *StoreWithContext {
|
||||||
|
return NewStoreWithContext(ctx, db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: prefix all these names with 'mock' since they're all mock stuff
|
||||||
|
|
||||||
|
type (
|
||||||
|
ListingCountsOverTimeReport struct {
|
||||||
|
AccountID int64
|
||||||
|
Platform accounts.Platform
|
||||||
|
ShopID string
|
||||||
|
ListingID string
|
||||||
|
Counts []ListingCountAtTime
|
||||||
|
}
|
||||||
|
|
||||||
|
ListingCountAtTime struct {
|
||||||
|
EventTimestamp *time.Time
|
||||||
|
Count int64
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r ListingCountsOverTimeReport) MaxCount() ListingCountAtTime {
|
||||||
|
if len(r.Counts) == 0 {
|
||||||
|
return ListingCountAtTime{}
|
||||||
|
}
|
||||||
|
return slices.MaxFunc(r.Counts, func(a, b ListingCountAtTime) int {
|
||||||
|
return int(a.Count - b.Count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ListingCountsOverTimeReport) MinCount() ListingCountAtTime {
|
||||||
|
if len(r.Counts) == 0 {
|
||||||
|
return ListingCountAtTime{}
|
||||||
|
}
|
||||||
|
return slices.MinFunc(r.Counts, func(a, b ListingCountAtTime) int {
|
||||||
|
return int(a.Count - b.Count)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) GetListingCountsReport(ctx context.Context, acctID int64, platform accounts.Platform, shopID, listingID string) (*ListingCountsOverTimeReport, error) {
|
||||||
|
counts, err := db.GetListingCountsOverTime(ctx, acctID, platform, shopID, listingID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ListingCountsOverTimeReport{
|
||||||
|
AccountID: acctID,
|
||||||
|
Platform: platform,
|
||||||
|
ShopID: shopID,
|
||||||
|
ListingID: listingID,
|
||||||
|
Counts: counts,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Store) GetListingCountsOverTime(ctx context.Context, acctID int64, platform accounts.Platform, shopID, listingID string) ([]ListingCountAtTime, error) {
|
||||||
|
info, ok := getMockShopSchemaInfo(platform)
|
||||||
|
if !ok {
|
||||||
|
return nil, consts.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var counts []ListingCountAtTime
|
||||||
|
|
||||||
|
err := pgx.BeginTxFunc(ctx, db.db, pgx.TxOptions{
|
||||||
|
AccessMode: pgx.ReadOnly,
|
||||||
|
}, func(tx pgx.Tx) error {
|
||||||
|
rows, err := tx.Query(
|
||||||
|
ctx,
|
||||||
|
fmt.Sprintf(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
%s
|
||||||
|
WHERE
|
||||||
|
shop_id = @shop_id
|
||||||
|
AND listing_id = @listing_id
|
||||||
|
`,
|
||||||
|
info.listingsTable,
|
||||||
|
),
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"shop_id": shopID,
|
||||||
|
"listing_id": listingID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform query to check the existing of the listing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[int]); err != nil {
|
||||||
|
return fmt.Errorf("listing not found: %w", consts.ErrNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err = tx.Query(
|
||||||
|
ctx,
|
||||||
|
fmt.Sprintf(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
event_timestamp AS eventTimestamp,
|
||||||
|
"count"
|
||||||
|
FROM
|
||||||
|
%s
|
||||||
|
WHERE
|
||||||
|
shop_id = @shop_id
|
||||||
|
AND listing_id = @listing_id
|
||||||
|
`,
|
||||||
|
info.listingCountsView,
|
||||||
|
),
|
||||||
|
pgx.NamedArgs{
|
||||||
|
"shop_id": shopID,
|
||||||
|
"listing_id": listingID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to perform query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if counts, err = pgx.CollectRows(rows, pgx.RowToStructByNameLax[ListingCountAtTime]); err != nil {
|
||||||
|
return fmt.Errorf("failed to scan rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return counts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockShopSchemaInfo struct {
|
||||||
|
platform accounts.Platform
|
||||||
|
shopTable string
|
||||||
|
listingsTable string
|
||||||
|
listingCountsView string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMockShopSchemaInfo(platform accounts.Platform) (mockShopSchemaInfo, bool) {
|
||||||
|
for _, in := range getAllMockShopSchemaInfos() {
|
||||||
|
if in.platform == platform {
|
||||||
|
return in, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockShopSchemaInfo{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllMockShopSchemaInfos() []mockShopSchemaInfo {
|
||||||
|
return []mockShopSchemaInfo{
|
||||||
|
{
|
||||||
|
platform: accounts.Amazon,
|
||||||
|
shopTable: "mock.shop_amazon",
|
||||||
|
listingsTable: "mock.shop_amazon_listings",
|
||||||
|
listingCountsView: "mock.shop_amazon_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.BigCartel,
|
||||||
|
shopTable: "mock.shop_big_cartel",
|
||||||
|
listingsTable: "mock.shop_big_cartel_listings",
|
||||||
|
listingCountsView: "mock.shop_big_cartel_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Ebay,
|
||||||
|
shopTable: "mock.shop_ebay",
|
||||||
|
listingsTable: "mock.shop_ebay_listings",
|
||||||
|
listingCountsView: "mock.shop_ebay_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Ecwid,
|
||||||
|
shopTable: "mock.shop_ecwid",
|
||||||
|
listingsTable: "mock.shop_ecwid_listings",
|
||||||
|
listingCountsView: "mock.shop_ecwid_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Etsy,
|
||||||
|
shopTable: "mock.shop_etsy",
|
||||||
|
listingsTable: "mock.shop_etsy_listings",
|
||||||
|
listingCountsView: "mock.shop_etsy_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Shopify,
|
||||||
|
shopTable: "mock.shop_shopify",
|
||||||
|
listingsTable: "mock.shop_shopify_listings",
|
||||||
|
listingCountsView: "mock.shop_shopify_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.SquareOnline,
|
||||||
|
shopTable: "mock.shop_square_online",
|
||||||
|
listingsTable: "mock.shop_square_online_listings",
|
||||||
|
listingCountsView: "mock.shop_square_online_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Squarespace,
|
||||||
|
shopTable: "mock.shop_squarespace",
|
||||||
|
listingsTable: "mock.shop_squarespace_listings",
|
||||||
|
listingCountsView: "mock.shop_squarespace_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Tiktok,
|
||||||
|
shopTable: "mock.shop_tiktok",
|
||||||
|
listingsTable: "mock.shop_tiktok_listings",
|
||||||
|
listingCountsView: "mock.shop_tiktok_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.WalmartMarketplace,
|
||||||
|
shopTable: "mock.shop_walmart_marketplace",
|
||||||
|
listingsTable: "mock.shop_walmart_marketplace_listings",
|
||||||
|
listingCountsView: "mock.shop_walmart_marketplace_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Wix,
|
||||||
|
shopTable: "mock.shop_wix",
|
||||||
|
listingsTable: "mock.shop_wix_listings",
|
||||||
|
listingCountsView: "mock.shop_wix_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.WooCommerce,
|
||||||
|
shopTable: "mock.shop_woo_commerce",
|
||||||
|
listingsTable: "mock.shop_woo_commerce_listings",
|
||||||
|
listingCountsView: "mock.shop_woo_commerce_listing_counts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platform: accounts.Zoho,
|
||||||
|
shopTable: "mock.shop_zoho",
|
||||||
|
listingsTable: "mock.shop_zoho_listings",
|
||||||
|
listingCountsView: "mock.shop_zoho_listing_counts",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Code generated by concurry DO NOT EDIT.
|
||||||
|
// https://github.com/angelbeltran/concurry
|
||||||
|
// concurry
|
||||||
|
package reports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"ruben/inventory2/domains/accounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreWithContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
*Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStoreWithContext(ctx context.Context, v *Store) *StoreWithContext {
|
||||||
|
return &StoreWithContext{
|
||||||
|
ctx: ctx,
|
||||||
|
Store: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetRawShopEvents(acctID int64, platform accounts.Platform, shopID string) ([]RawShopEvent, error) {
|
||||||
|
return v_ctx.Store.GetRawShopEvents(v_ctx.ctx, acctID, platform, shopID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetListingCountsReport(acctID int64, platform accounts.Platform, shopID string, listingID string) (*ListingCountsOverTimeReport, error) {
|
||||||
|
return v_ctx.Store.GetListingCountsReport(v_ctx.ctx, acctID, platform, shopID, listingID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v_ctx *StoreWithContext) GetListingCountsOverTime(acctID int64, platform accounts.Platform, shopID string, listingID string) ([]ListingCountAtTime, error) {
|
||||||
|
return v_ctx.Store.GetListingCountsOverTime(v_ctx.ctx, acctID, platform, shopID, listingID)
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 507 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 368 B |
|
After Width: | Height: | Size: 756 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 539 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -22,11 +22,14 @@ import (
|
|||||||
"github.com/lmittmann/tint"
|
"github.com/lmittmann/tint"
|
||||||
|
|
||||||
"ruben/inventory2/domains/accounts"
|
"ruben/inventory2/domains/accounts"
|
||||||
|
"ruben/inventory2/domains/amazon"
|
||||||
"ruben/inventory2/domains/authentication"
|
"ruben/inventory2/domains/authentication"
|
||||||
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
||||||
"ruben/inventory2/domains/raw_events"
|
"ruben/inventory2/domains/raw_events"
|
||||||
|
"ruben/inventory2/domains/reports"
|
||||||
"ruben/inventory2/logging"
|
"ruben/inventory2/logging"
|
||||||
"ruben/inventory2/server"
|
"ruben/inventory2/server"
|
||||||
|
"ruben/inventory2/server/sse"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -72,18 +75,39 @@ func runApp(ctx context.Context, logger *logging.Logger) error {
|
|||||||
return fmt.Errorf("failed to initialize database connection pool: %w", err)
|
return fmt.Errorf("failed to initialize database connection pool: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// start background processes
|
|
||||||
|
|
||||||
auth, err := authentication.New(ctx, connPool, logger.WithGroup("authenticator"))
|
auth, err := authentication.New(ctx, connPool, logger.WithGroup("authenticator"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to construct authenticator: %w", err)
|
return fmt.Errorf("failed to construct authenticator: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
authErrCh := runAuthProcesses(ctx, auth)
|
accts := accounts.NewStore(logger.WithGroup("accounts"), connPool)
|
||||||
|
|
||||||
// start http server
|
// start http server
|
||||||
|
|
||||||
srvErrCh := runServer(ctx, logger, connPool, auth)
|
sseQueue, srvErrCh := runServer(ctx, logger, connPool, auth, accts)
|
||||||
|
|
||||||
|
// start background processes
|
||||||
|
|
||||||
|
authErrCh := runAuthProcesses(ctx, auth)
|
||||||
|
|
||||||
|
eventErrCh := runEventProcessing(
|
||||||
|
ctx,
|
||||||
|
amazon.NewMocks(logger.WithGroup("amazon"), connPool).
|
||||||
|
SetListener(sseQueue.NewDBEventPublisher(
|
||||||
|
func(ctx context.Context, e raw_events.Event) (acctID int64, err error) {
|
||||||
|
p, err := accounts.NewPlatform(e.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accts.GetAccountIDByMockPlatformAndShopID(ctx, p, e.StoreID)
|
||||||
|
},
|
||||||
|
func(acctID int64, e raw_events.Event) (eventType string, err error) {
|
||||||
|
// TODO: fine tune the event type later (don't want a referesh on EVERY event)
|
||||||
|
return fmt.Sprintf("accounts_%d_simulations", acctID), nil
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
// wait for interrupt signal or unrecoverable failure, then shutdown
|
// wait for interrupt signal or unrecoverable failure, then shutdown
|
||||||
|
|
||||||
@@ -92,8 +116,9 @@ func runApp(ctx context.Context, logger *logging.Logger) error {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
alreadyShutdown struct {
|
alreadyShutdown struct {
|
||||||
server bool
|
server bool
|
||||||
authProcesses bool
|
authProcesses bool
|
||||||
|
eventProcessing bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
select {
|
select {
|
||||||
@@ -106,6 +131,12 @@ func runApp(ctx context.Context, logger *logging.Logger) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("auth processes encountered error", "error", err)
|
logger.Error("auth processes encountered error", "error", err)
|
||||||
}
|
}
|
||||||
|
case err := <-eventErrCh:
|
||||||
|
alreadyShutdown.eventProcessing = true
|
||||||
|
logger.Error("event processing shutdown unexpectedly")
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("event processing encountered error", "error", err)
|
||||||
|
}
|
||||||
case err := <-srvErrCh:
|
case err := <-srvErrCh:
|
||||||
alreadyShutdown.server = true
|
alreadyShutdown.server = true
|
||||||
logger.Error("server shutdown unexpectedly")
|
logger.Error("server shutdown unexpectedly")
|
||||||
@@ -127,6 +158,13 @@ func runApp(ctx context.Context, logger *logging.Logger) error {
|
|||||||
logger.Info("auth processes shut down")
|
logger.Info("auth processes shut down")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !alreadyShutdown.eventProcessing {
|
||||||
|
if err := <-authErrCh; err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("event processing experienced an error: %w", err))
|
||||||
|
}
|
||||||
|
logger.Info("event processing shut down")
|
||||||
|
}
|
||||||
|
|
||||||
if !alreadyShutdown.server {
|
if !alreadyShutdown.server {
|
||||||
if err := <-srvErrCh; err != nil {
|
if err := <-srvErrCh; err != nil {
|
||||||
errs = append(errs, fmt.Errorf("server experienced an error: %w", err))
|
errs = append(errs, fmt.Errorf("server experienced an error: %w", err))
|
||||||
@@ -150,12 +188,32 @@ func runAuthProcesses(ctx context.Context, auth *authentication.Authenticator) <
|
|||||||
return errCh
|
return errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServer(ctx context.Context, logger *logging.Logger, connPool *pgxpool.Pool, auth *authentication.Authenticator) <-chan error {
|
func runEventProcessing(ctx context.Context, amz *amazon.Mocks) <-chan error {
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
defer close(errCh)
|
||||||
|
|
||||||
|
if err := amz.ProcessEvents(ctx); err != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return errCh
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServer(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *logging.Logger,
|
||||||
|
connPool *pgxpool.Pool,
|
||||||
|
auth *authentication.Authenticator,
|
||||||
|
accts *accounts.Store,
|
||||||
|
) (*sse.Queue, <-chan error) {
|
||||||
r := server.NewRouter(
|
r := server.NewRouter(
|
||||||
logger.WithGroup("server"),
|
logger.WithGroup("server"),
|
||||||
"./",
|
"./",
|
||||||
raw_events.NewStore(logger.WithGroup("raw-event-store"), connPool),
|
raw_events.NewStore(logger.WithGroup("raw-event-store"), connPool),
|
||||||
accounts.NewStore(logger, connPool),
|
accts,
|
||||||
|
reports.NewStore(logger, connPool, accts),
|
||||||
etsy_platform.NewPlatform(
|
etsy_platform.NewPlatform(
|
||||||
logger,
|
logger,
|
||||||
func(acctID int64) string {
|
func(acctID int64) string {
|
||||||
@@ -231,5 +289,5 @@ func runServer(ctx context.Context, logger *logging.Logger, connPool *pgxpool.Po
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return errCh
|
return r.GetSSEQueue(), errCh
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
(function(){
|
||||||
|
const defaultDebounceTimeInMs = 500;
|
||||||
|
|
||||||
|
let isReady = false
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
isReady = true
|
||||||
|
})
|
||||||
|
function onReady(fn) {
|
||||||
|
if (isReady || document.readyState === 'complete') {
|
||||||
|
fn()
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
htmx.defineExtension("resize", {
|
||||||
|
init: (api) => {
|
||||||
|
function hasResizeTrigger(elt) {
|
||||||
|
for (spec of api.getTriggerSpecs(elt)) {
|
||||||
|
if (spec.trigger === 'resize') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDebounceTimeInMs(elt) {
|
||||||
|
const amountAttr = elt.attributes['hx-resize-debounce'];
|
||||||
|
if (!amountAttr) {
|
||||||
|
return defaultDebounceTimeInMs
|
||||||
|
}
|
||||||
|
const amountStr = amountAttr.value
|
||||||
|
if (!amountStr) {
|
||||||
|
return defaultDebounceTimeInMs
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return parseInt(amountStr);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('invalid debounce time on trigger:', trigger);
|
||||||
|
console.error(err);
|
||||||
|
return defaultDebounceTimeInMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextID = 1;
|
||||||
|
let timeoutIDs = {};
|
||||||
|
let observers = {};
|
||||||
|
function processNode(elt) {
|
||||||
|
if (!hasResizeTrigger(elt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = elt['hx-resize-internal-data'];
|
||||||
|
if (!data) {
|
||||||
|
data = {
|
||||||
|
id: nextID,
|
||||||
|
};
|
||||||
|
nextID += 1;
|
||||||
|
|
||||||
|
elt['hx-resize-internal-data'] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const obs = new ResizeObserver(() => {
|
||||||
|
if (timeoutIDs[data.id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debounceTime = getDebounceTimeInMs(elt);
|
||||||
|
timeoutIDs[data.id] = setTimeout(() => {
|
||||||
|
elt.dispatchEvent(new Event('resize'));
|
||||||
|
delete timeoutIDs[data.id];
|
||||||
|
}, debounceTime);
|
||||||
|
});
|
||||||
|
|
||||||
|
obs.observe(elt);
|
||||||
|
|
||||||
|
observers[data.id] = obs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupNode(elt) {
|
||||||
|
if (!hasResizeTrigger(elt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = elt['hx-resize-internal-data'];
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const obs = observers[data.id];
|
||||||
|
if (obs) {
|
||||||
|
obs.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete observers[data.id];
|
||||||
|
delete timeoutIDs[data.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// process nodes
|
||||||
|
htmx.on('htmx:beforeProcessNode', evt => processNode(evt.target));
|
||||||
|
htmx.on('htmx:beforeCleanupElement', evt => cleanupNode(evt.target));
|
||||||
|
onReady(function() {
|
||||||
|
document.querySelectorAll('[hx-trigger]').forEach(processNode);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})()
|
||||||
@@ -3,36 +3,43 @@ package accounts
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"ruben/inventory2/consts"
|
"ruben/inventory2/consts"
|
||||||
"ruben/inventory2/domains/accounts"
|
"ruben/inventory2/domains/accounts"
|
||||||
|
"ruben/inventory2/domains/reports"
|
||||||
"ruben/inventory2/logging"
|
"ruben/inventory2/logging"
|
||||||
"ruben/inventory2/server/auth"
|
"ruben/inventory2/server/auth"
|
||||||
"ruben/inventory2/server/param"
|
"ruben/inventory2/server/param"
|
||||||
"ruben/inventory2/server/response"
|
"ruben/inventory2/server/response"
|
||||||
"ruben/inventory2/server/sse"
|
"ruben/inventory2/server/sse"
|
||||||
|
"ruben/inventory2/server/ui/charts"
|
||||||
)
|
)
|
||||||
|
|
||||||
type accountSubrouter struct {
|
type accountSubrouter struct {
|
||||||
log *logging.Logger
|
log *logging.Logger
|
||||||
accts *accounts.Store
|
accts *accounts.Store
|
||||||
pub *sse.UpdateNotificationPublisher
|
reports *reports.Store
|
||||||
|
pub *sse.UpdateNotificationPublisher
|
||||||
}
|
}
|
||||||
|
|
||||||
func Routes(
|
func Routes(
|
||||||
r *gin.RouterGroup,
|
r *gin.RouterGroup,
|
||||||
logger *logging.Logger,
|
logger *logging.Logger,
|
||||||
accts *accounts.Store,
|
accts *accounts.Store,
|
||||||
|
reports *reports.Store,
|
||||||
pub *sse.UpdateNotificationPublisher,
|
pub *sse.UpdateNotificationPublisher,
|
||||||
) {
|
) {
|
||||||
as := &accountSubrouter{
|
as := &accountSubrouter{
|
||||||
log: logger,
|
log: logger,
|
||||||
accts: accts,
|
accts: accts,
|
||||||
pub: pub,
|
reports: reports,
|
||||||
|
pub: pub,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.POST("", response.Handler(as.createAccount))
|
r.POST("", response.Handler(as.createAccount))
|
||||||
@@ -46,6 +53,9 @@ func Routes(
|
|||||||
mockShops.PUT("/listings/:listing-id", response.Handler(as.updateMockListing))
|
mockShops.PUT("/listings/:listing-id", response.Handler(as.updateMockListing))
|
||||||
mockShops.DELETE("/listings/:listing-id", response.Handler(as.deleteMockListing))
|
mockShops.DELETE("/listings/:listing-id", response.Handler(as.deleteMockListing))
|
||||||
|
|
||||||
|
mockListingCharts := r.Group("/:acctID/platforms/:platform/shops/mocks/:shop-id/listings/:listing-id/charts")
|
||||||
|
mockListingCharts.GET("/counts", response.Handler(as.getCountsChartForMockListing))
|
||||||
|
|
||||||
syncGroups := r.Group("/:acctID/inventory/sync-groups", pub.Publish("/:acctID/inventory/sync-groups"))
|
syncGroups := r.Group("/:acctID/inventory/sync-groups", pub.Publish("/:acctID/inventory/sync-groups"))
|
||||||
syncGroups.POST("", response.Handler(as.saveNewSyncGroup))
|
syncGroups.POST("", response.Handler(as.saveNewSyncGroup))
|
||||||
|
|
||||||
@@ -78,6 +88,14 @@ func Routes(
|
|||||||
mockSyncGroupBeingEdited.PUT("/listings/:orderIndex/shop", response.Handler(as.setShopInListingInMockSyncGroupBeingEdited))
|
mockSyncGroupBeingEdited.PUT("/listings/:orderIndex/shop", response.Handler(as.setShopInListingInMockSyncGroupBeingEdited))
|
||||||
mockSyncGroupBeingEdited.PUT("/listings/:orderIndex/listing", response.Handler(as.setListingInListingInMockSyncGroupBeingEdited))
|
mockSyncGroupBeingEdited.PUT("/listings/:orderIndex/listing", response.Handler(as.setListingInListingInMockSyncGroupBeingEdited))
|
||||||
mockSyncGroupBeingEdited.DELETE("/listings/:orderIndex", response.Handler(as.deleteListingInListingInMockSyncGroupBeingEdited))
|
mockSyncGroupBeingEdited.DELETE("/listings/:orderIndex", response.Handler(as.deleteListingInListingInMockSyncGroupBeingEdited))
|
||||||
|
|
||||||
|
simulations := r.Group("/:acctID/simulations", pub.Publish("/:acctID/simulations"))
|
||||||
|
simulations.PUT("/sale/form/shop", response.Handler(as.setShopInMockSaleForm))
|
||||||
|
simulations.POST("/sale", response.Handler(as.postNewMockSale))
|
||||||
|
simulations.PUT("/refund/form/shop", response.Handler(as.setShopInMockRefundForm))
|
||||||
|
simulations.POST("/refund", response.Handler(as.postNewMockRefund))
|
||||||
|
simulations.PUT("/count/form/shop", response.Handler(as.setShopInMockCountForm))
|
||||||
|
simulations.PUT("/shops/:shopID/listings/:listingID/count", response.Handler(as.setMockListingCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /
|
// POST /
|
||||||
@@ -413,6 +431,135 @@ func (s *accountSubrouter) deleteMockListing(c *gin.Context) (response.Response,
|
|||||||
return response.StatusNoContent(), nil
|
return response.StatusNoContent(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: accept dimensions
|
||||||
|
// GET /:acctID/platforms/:platform/shops/mocks/:shop-id/charts/counts
|
||||||
|
func (s *accountSubrouter) getCountsChartForMockListing(c *gin.Context) (response.Response, error) {
|
||||||
|
s.log.Debug("ENDPOINT HIT")
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
listingID string
|
||||||
|
width float64
|
||||||
|
height float64
|
||||||
|
timezone string
|
||||||
|
)
|
||||||
|
|
||||||
|
err := param.Path("platform", param.Platform(&platform)).
|
||||||
|
Path("shop-id", param.Text(&shopID)).
|
||||||
|
Path("listing-id", param.Text(&listingID)).
|
||||||
|
Form("width", param.Float64(&width)).
|
||||||
|
Form("height", param.Float64(&height)).
|
||||||
|
Form("timezone", param.Text(&timezone)).
|
||||||
|
Unmarshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if width <= 0 {
|
||||||
|
return nil, response.BadRequest().Msg("width must be positive")
|
||||||
|
}
|
||||||
|
if height <= 0 {
|
||||||
|
return nil, response.BadRequest().Msg("height must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
report, err := s.reports.GetListingCountsReport(c, acctID, platform, shopID, listingID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get report: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
bvs := make([]charts.BarValue, len(report.Counts))
|
||||||
|
for i, v := range report.Counts {
|
||||||
|
label := "start"
|
||||||
|
if v.EventTimestamp != nil {
|
||||||
|
label = v.EventTimestamp.Format(time.RFC822)
|
||||||
|
}
|
||||||
|
bvs[i] = charts.BarValue{
|
||||||
|
Label: label,
|
||||||
|
Value: float64(v.Count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusOK().
|
||||||
|
HTMLReader(charts.NewBar(bvs...).SVG().GetMarkupReader()), nil
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
lvs := make([]charts.LineValue, len(report.Counts))
|
||||||
|
for i, v := range report.Counts {
|
||||||
|
label := "initial"
|
||||||
|
if v.EventTimestamp != nil {
|
||||||
|
label = v.EventTimestamp.Format(time.RFC822)
|
||||||
|
}
|
||||||
|
lvs[i] = charts.LineValue{
|
||||||
|
Label: label,
|
||||||
|
Value: float64(v.Count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
l, err := time.LoadLocation(timezone)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Errorf("failed to parse time zone %s: %v; defaulting to UTC", timezone, err)
|
||||||
|
l = time.UTC
|
||||||
|
}
|
||||||
|
|
||||||
|
var times []time.Time
|
||||||
|
for _, v := range report.Counts {
|
||||||
|
if v.EventTimestamp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
times = append(times, *v.EventTimestamp)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
minTime time.Time
|
||||||
|
maxTime time.Time
|
||||||
|
initTime time.Time
|
||||||
|
)
|
||||||
|
if len(times) > 0 {
|
||||||
|
minTime = slices.MinFunc(times, func(a, b time.Time) int {
|
||||||
|
return int(a.UnixNano() - b.UnixNano())
|
||||||
|
}).In(l)
|
||||||
|
maxTime = slices.MaxFunc(times, func(a, b time.Time) int {
|
||||||
|
return int(a.UnixNano() - b.UnixNano())
|
||||||
|
}).In(l)
|
||||||
|
|
||||||
|
if len(times) > 1 {
|
||||||
|
avgIntervalPerUpdate := time.Duration((maxTime.UnixNano() - minTime.UnixNano())) / time.Duration(len(times)-1)
|
||||||
|
initTime = minTime.Add(-avgIntervalPerUpdate)
|
||||||
|
} else {
|
||||||
|
initTime = minTime.Add(-time.Minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lvs := make([]charts.TimeLineValue, len(report.Counts))
|
||||||
|
for i, v := range report.Counts {
|
||||||
|
ts := initTime
|
||||||
|
label := fmt.Sprint(v.Count)
|
||||||
|
if v.EventTimestamp != nil {
|
||||||
|
ts = v.EventTimestamp.In(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
lvs[i] = charts.TimeLineValue{
|
||||||
|
Label: label,
|
||||||
|
Time: ts,
|
||||||
|
Value: float64(v.Count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusOK().
|
||||||
|
HTMLReader(
|
||||||
|
//charts.NewLineChart(width, height, lvs...).
|
||||||
|
charts.NewTimeLineChart(width, height, lvs...).
|
||||||
|
Foreground("var(--foreground)").
|
||||||
|
Background("var(--muted)").
|
||||||
|
SVG().
|
||||||
|
GetMarkupReader(),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
// POST /:acctID/inventory/sync-groups/mock/draft/listings
|
// POST /:acctID/inventory/sync-groups/mock/draft/listings
|
||||||
func (s *accountSubrouter) createMockSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
|
func (s *accountSubrouter) createMockSyncGroupListingDraft(c *gin.Context) (response.Response, error) {
|
||||||
r := c.Request
|
r := c.Request
|
||||||
@@ -736,6 +883,150 @@ func (s *accountSubrouter) deleteListingInListingInMockSyncGroupBeingEdited(c *g
|
|||||||
return response.StatusOK(), nil
|
return response.StatusOK(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT /:acctID/simulations/sale/form/shop
|
||||||
|
func (s *accountSubrouter) setShopInMockSaleForm(c *gin.Context) (response.Response, error) {
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := param.Form("platform", param.Platform(&platform)).
|
||||||
|
Form("shop-id", param.Text(&shopID)).
|
||||||
|
Unmarshal(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.accts.SetShopInMockSaleForm(c, acctID, platform, shopID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set shop in mock sale form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusOK(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /:acctID/simulations/sale
|
||||||
|
func (s *accountSubrouter) postNewMockSale(c *gin.Context) (response.Response, error) {
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
listingID string
|
||||||
|
count int
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := param.Form("platform", param.Platform(&platform)).
|
||||||
|
Form("shop-id", param.Text(&shopID)).
|
||||||
|
Form("listing-id", param.Text(&listingID)).
|
||||||
|
Form("count", param.Int(&count)).
|
||||||
|
Unmarshal(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.accts.SaveNewMockSale(c, acctID, platform, shopID, listingID, count); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save new mock sale: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusCreated(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /:acctID/simulations/refund/form/shop
|
||||||
|
func (s *accountSubrouter) setShopInMockRefundForm(c *gin.Context) (response.Response, error) {
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := param.Form("platform", param.Platform(&platform)).
|
||||||
|
Form("shop-id", param.Text(&shopID)).
|
||||||
|
Unmarshal(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.accts.SetShopInMockRefundForm(c, acctID, platform, shopID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set shop in mock refund form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusOK(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /:acctID/simulations/refund
|
||||||
|
func (s *accountSubrouter) postNewMockRefund(c *gin.Context) (response.Response, error) {
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
listingID string
|
||||||
|
count int
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := param.Form("platform", param.Platform(&platform)).
|
||||||
|
Form("shop-id", param.Text(&shopID)).
|
||||||
|
Form("listing-id", param.Text(&listingID)).
|
||||||
|
Form("count", param.Int(&count)).
|
||||||
|
Unmarshal(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.accts.SaveNewMockRefund(c, acctID, platform, shopID, listingID, count); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save new mock refund: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusCreated(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /:acctID/simulations/count/form/shop
|
||||||
|
func (s *accountSubrouter) setShopInMockCountForm(c *gin.Context) (response.Response, error) {
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := param.Form("platform", param.Platform(&platform)).
|
||||||
|
Form("shop-id", param.Text(&shopID)).
|
||||||
|
Unmarshal(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.accts.SetShopInMockCountForm(c, acctID, platform, shopID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to set shop in mock count form: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusOK(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /:acctID/simulations/shops/:shopID/listings/:listingID/count
|
||||||
|
func (s *accountSubrouter) setMockListingCount(c *gin.Context) (response.Response, error) {
|
||||||
|
acctID := auth.GetIdentity(c).Account.AccountID
|
||||||
|
|
||||||
|
var (
|
||||||
|
platform accounts.Platform
|
||||||
|
shopID string
|
||||||
|
listingID string
|
||||||
|
count int
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := param.Form("platform", param.Platform(&platform)).
|
||||||
|
Path("shopID", param.Text(&shopID)).
|
||||||
|
Path("listingID", param.Text(&listingID)).
|
||||||
|
Form("count", param.Int(&count)).
|
||||||
|
Unmarshal(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.accts.SaveNewMockInventoryReset(c, acctID, platform, shopID, listingID, count); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save new mock inventory reset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.StatusOK(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func lowerSnakeCase(s accounts.Platform) string {
|
func lowerSnakeCase(s accounts.Platform) string {
|
||||||
return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_"))
|
return strings.ToLower(strings.Join(strings.Split(string(s), " "), "_"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"ruben/inventory2/domains/accounts"
|
"ruben/inventory2/domains/accounts"
|
||||||
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
||||||
"ruben/inventory2/domains/raw_events"
|
"ruben/inventory2/domains/raw_events"
|
||||||
|
"ruben/inventory2/domains/reports"
|
||||||
"ruben/inventory2/logging"
|
"ruben/inventory2/logging"
|
||||||
accounts_api "ruben/inventory2/server/api/accounts"
|
accounts_api "ruben/inventory2/server/api/accounts"
|
||||||
auth_api "ruben/inventory2/server/api/auth"
|
auth_api "ruben/inventory2/server/api/auth"
|
||||||
@@ -22,6 +23,7 @@ func Routes(
|
|||||||
auth *auth.Service,
|
auth *auth.Service,
|
||||||
sq *sse.Queue,
|
sq *sse.Queue,
|
||||||
accts *accounts.Store,
|
accts *accounts.Store,
|
||||||
|
reps *reports.Store,
|
||||||
unp *sse.UpdateNotificationPublisher,
|
unp *sse.UpdateNotificationPublisher,
|
||||||
rawEvents *raw_events.Store,
|
rawEvents *raw_events.Store,
|
||||||
etsy *etsy_platform.Platform,
|
etsy *etsy_platform.Platform,
|
||||||
@@ -40,6 +42,7 @@ func Routes(
|
|||||||
r.Group("/accounts", auth.Authenticate()),
|
r.Group("/accounts", auth.Authenticate()),
|
||||||
logger.WithGroup("/accounts"),
|
logger.WithGroup("/accounts"),
|
||||||
accts,
|
accts,
|
||||||
|
reps,
|
||||||
unp.Group("/accounts"),
|
unp.Group("/accounts"),
|
||||||
)
|
)
|
||||||
webhooks.Routes(
|
webhooks.Routes(
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ func Int64(dst *int64) encoding.TextUnmarshaler {
|
|||||||
return (*int64Text)(dst)
|
return (*int64Text)(dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Float64(dst *float64) encoding.TextUnmarshaler {
|
||||||
|
return (*float64Text)(dst)
|
||||||
|
}
|
||||||
|
|
||||||
func Bool(dst *bool) encoding.TextUnmarshaler {
|
func Bool(dst *bool) encoding.TextUnmarshaler {
|
||||||
return (*boolText)(dst)
|
return (*boolText)(dst)
|
||||||
}
|
}
|
||||||
@@ -33,6 +37,7 @@ type (
|
|||||||
rawText string
|
rawText string
|
||||||
intText int
|
intText int
|
||||||
int64Text int64
|
int64Text int64
|
||||||
|
float64Text float64
|
||||||
boolText bool
|
boolText bool
|
||||||
platformText accounts.Platform
|
platformText accounts.Platform
|
||||||
)
|
)
|
||||||
@@ -60,6 +65,15 @@ func (n *int64Text) UnmarshalText(text []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *float64Text) UnmarshalText(text []byte) error {
|
||||||
|
i, err := strconv.ParseFloat(string(text), 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*n = float64Text(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (b *boolText) UnmarshalText(text []byte) error {
|
func (b *boolText) UnmarshalText(text []byte) error {
|
||||||
v, err := strconv.ParseBool(string(text))
|
v, err := strconv.ParseBool(string(text))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ func (s Spec) Unmarshal(c *gin.Context) error {
|
|||||||
for k, dst := range s.form {
|
for k, dst := range s.form {
|
||||||
v, ok := c.GetPostForm(k)
|
v, ok := c.GetPostForm(k)
|
||||||
if !ok || v == "" {
|
if !ok || v == "" {
|
||||||
return response.BadRequest().Msgf("no %s provided", k)
|
if v, ok = c.GetQuery(k); !ok {
|
||||||
|
return response.BadRequest().Msgf("no %s provided", k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := dst.UnmarshalText([]byte(v)); err != nil {
|
if err := dst.UnmarshalText([]byte(v)); err != nil {
|
||||||
return response.BadRequest().Wrap(err).Msgf("invalid %s provided", k)
|
return response.BadRequest().Wrap(err).Msgf("invalid %s provided", k)
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ func (b bodyRes) HTML(body []byte) Response {
|
|||||||
return HTML(body).wrap(b)
|
return HTML(body).wrap(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b bodyRes) HTMLReader(body io.Reader) Response {
|
||||||
|
return HTMLReader(body).wrap(b)
|
||||||
|
}
|
||||||
|
|
||||||
func (b bodyRes) JSON(body any) Response {
|
func (b bodyRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(b)
|
return JSON(body).wrap(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ func (c cookieRes) HTML(body []byte) Response {
|
|||||||
return HTML(body).wrap(c)
|
return HTML(body).wrap(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c cookieRes) HTMLReader(body io.Reader) Response {
|
||||||
|
return HTMLReader(body).wrap(c)
|
||||||
|
}
|
||||||
|
|
||||||
func (c cookieRes) JSON(body any) Response {
|
func (c cookieRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(c)
|
return JSON(body).wrap(c)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ func (h headerRes) HTML(body []byte) Response {
|
|||||||
return HTML(body).wrap(h)
|
return HTML(body).wrap(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h headerRes) HTMLReader(body io.Reader) Response {
|
||||||
|
return HTMLReader(body).wrap(h)
|
||||||
|
}
|
||||||
|
|
||||||
func (h headerRes) JSON(body any) Response {
|
func (h headerRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(h)
|
return JSON(body).wrap(h)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
htmlRes struct {
|
htmlRes struct {
|
||||||
body []byte
|
body []byte
|
||||||
res Response
|
reader io.Reader
|
||||||
|
res Response
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +25,12 @@ func HTML(body []byte) Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HTMLReader(body io.Reader) Response {
|
||||||
|
return htmlRes{
|
||||||
|
reader: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h htmlRes) String() string {
|
func (h htmlRes) String() string {
|
||||||
if h.res != nil {
|
if h.res != nil {
|
||||||
return fmt.Sprintf(`{"body": %q, "nested": %s}`, string(h.body), h.res)
|
return fmt.Sprintf(`{"body": %q, "nested": %s}`, string(h.body), h.res)
|
||||||
@@ -46,6 +53,13 @@ func (h htmlRes) Redirect(code redirect.Code, to string) Response {
|
|||||||
|
|
||||||
func (h htmlRes) HTML(body []byte) Response {
|
func (h htmlRes) HTML(body []byte) Response {
|
||||||
h.body = body
|
h.body = body
|
||||||
|
h.reader = nil
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h htmlRes) HTMLReader(body io.Reader) Response {
|
||||||
|
h.body = nil
|
||||||
|
h.reader = body
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +147,13 @@ func (h htmlRes) GetRedirect() (code redirect.Code, to string, ok bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h htmlRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
func (h htmlRes) getBody() (body io.ReadCloser, ok bool, err error) {
|
||||||
return io.NopCloser(bytes.NewBuffer(h.body)), true, nil
|
var r io.Reader
|
||||||
|
if h.reader != nil {
|
||||||
|
r = h.reader
|
||||||
|
} else {
|
||||||
|
r = bytes.NewBuffer(h.body)
|
||||||
|
}
|
||||||
|
return io.NopCloser(r), true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h htmlRes) getCookies() []http.Cookie {
|
func (h htmlRes) getCookies() []http.Cookie {
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ func (j jsonRes) HTML(body []byte) Response {
|
|||||||
return HTML(body).wrap(j)
|
return HTML(body).wrap(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j jsonRes) HTMLReader(body io.Reader) Response {
|
||||||
|
return HTMLReader(body).wrap(j)
|
||||||
|
}
|
||||||
|
|
||||||
func (j jsonRes) JSON(body any) Response {
|
func (j jsonRes) JSON(body any) Response {
|
||||||
j.body = body
|
j.body = body
|
||||||
return j
|
return j
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ func (r redirectRes) HTML(body []byte) Response {
|
|||||||
return HTML(body).wrap(r)
|
return HTML(body).wrap(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r redirectRes) HTMLReader(body io.Reader) Response {
|
||||||
|
return HTMLReader(body).wrap(r)
|
||||||
|
}
|
||||||
|
|
||||||
func (r redirectRes) JSON(body any) Response {
|
func (r redirectRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(r)
|
return JSON(body).wrap(r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type (
|
|||||||
Redirect(code redirect.Code, to string) Response
|
Redirect(code redirect.Code, to string) Response
|
||||||
Body(io.ReadCloser) Response
|
Body(io.ReadCloser) Response
|
||||||
HTML([]byte) Response
|
HTML([]byte) Response
|
||||||
|
HTMLReader(io.Reader) Response
|
||||||
JSON(any) Response
|
JSON(any) Response
|
||||||
Cookie(http.Cookie) Response
|
Cookie(http.Cookie) Response
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ func (s statusRes) HTML(body []byte) Response {
|
|||||||
return HTML(body).wrap(s)
|
return HTML(body).wrap(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s statusRes) HTMLReader(body io.Reader) Response {
|
||||||
|
return HTMLReader(body).wrap(s)
|
||||||
|
}
|
||||||
|
|
||||||
func (s statusRes) JSON(body any) Response {
|
func (s statusRes) JSON(body any) Response {
|
||||||
return JSON(body).wrap(s)
|
return JSON(body).wrap(s)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"ruben/inventory2/domains/authentication"
|
"ruben/inventory2/domains/authentication"
|
||||||
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
||||||
"ruben/inventory2/domains/raw_events"
|
"ruben/inventory2/domains/raw_events"
|
||||||
|
"ruben/inventory2/domains/reports"
|
||||||
"ruben/inventory2/logging"
|
"ruben/inventory2/logging"
|
||||||
"ruben/inventory2/server/api"
|
"ruben/inventory2/server/api"
|
||||||
"ruben/inventory2/server/auth"
|
"ruben/inventory2/server/auth"
|
||||||
@@ -30,6 +31,7 @@ func NewRouter(
|
|||||||
contentDir string,
|
contentDir string,
|
||||||
rawEvents *raw_events.Store,
|
rawEvents *raw_events.Store,
|
||||||
accts *accounts.Store,
|
accts *accounts.Store,
|
||||||
|
reps *reports.Store,
|
||||||
etsy *etsy_platform.Platform,
|
etsy *etsy_platform.Platform,
|
||||||
authr *authentication.Authenticator,
|
authr *authentication.Authenticator,
|
||||||
) *Router {
|
) *Router {
|
||||||
@@ -59,6 +61,7 @@ func NewRouter(
|
|||||||
"/ui",
|
"/ui",
|
||||||
rawEvents,
|
rawEvents,
|
||||||
accts,
|
accts,
|
||||||
|
reps,
|
||||||
etsy,
|
etsy,
|
||||||
authM.Authenticate(),
|
authM.Authenticate(),
|
||||||
)
|
)
|
||||||
@@ -92,6 +95,7 @@ func NewRouter(
|
|||||||
authM,
|
authM,
|
||||||
sq,
|
sq,
|
||||||
accts,
|
accts,
|
||||||
|
reps,
|
||||||
unp,
|
unp,
|
||||||
rawEvents,
|
rawEvents,
|
||||||
etsy,
|
etsy,
|
||||||
@@ -107,6 +111,10 @@ func (r *Router) RunSSE(ctx context.Context) error {
|
|||||||
return r.sse.Start(ctx)
|
return r.sse.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Router) GetSSEQueue() *sse.Queue {
|
||||||
|
return r.sse
|
||||||
|
}
|
||||||
|
|
||||||
func fileServer(urlPrefix, dir string, beforeServe func(c *gin.Context)) gin.HandlerFunc {
|
func fileServer(urlPrefix, dir string, beforeServe func(c *gin.Context)) gin.HandlerFunc {
|
||||||
scfs := http.StripPrefix(urlPrefix, http.FileServer(http.Dir(dir)))
|
scfs := http.StripPrefix(urlPrefix, http.FileServer(http.Dir(dir)))
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package sse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"ruben/inventory2/domains/raw_events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
DBEventPublisher struct {
|
||||||
|
queue sender
|
||||||
|
getAccountID func(context.Context, raw_events.Event) (int64, error)
|
||||||
|
getEventType func(acctID int64, e raw_events.Event) (string, error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (q *Queue) NewDBEventPublisher(
|
||||||
|
getAccountID func(context.Context, raw_events.Event) (int64, error),
|
||||||
|
getEventType func(acctID int64, e raw_events.Event) (string, error),
|
||||||
|
) *DBEventPublisher {
|
||||||
|
return &DBEventPublisher{
|
||||||
|
queue: q,
|
||||||
|
getAccountID: getAccountID,
|
||||||
|
getEventType: getEventType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DBEventPublisher) Notify(ctx context.Context, e raw_events.Event) error {
|
||||||
|
acctID, err := p.getAccountID(ctx, e)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get account id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
et, err := p.getEventType(acctID, e)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compute event type: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.queue.Send(ctx, StandardEvent(acctID, et)); err != nil {
|
||||||
|
return fmt.Errorf("failed to send sse event to listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -22,11 +22,6 @@ type (
|
|||||||
trimBasePath string
|
trimBasePath string
|
||||||
basePathPattern string
|
basePathPattern string
|
||||||
}
|
}
|
||||||
|
|
||||||
// sender is satisfied by *Queue
|
|
||||||
sender interface {
|
|
||||||
Send(ctx context.Context, e Event) error
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (q *Queue) NewUpdateNotificationPublisher(
|
func (q *Queue) NewUpdateNotificationPublisher(
|
||||||
@@ -110,11 +105,7 @@ func (p *UpdateNotificationPublisher) Publish(pathPattern string) gin.HandlerFun
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := p.queue.Send(ctx, Event{
|
if err := p.queue.Send(ctx, StandardEvent(acctID, e)); err != nil {
|
||||||
AccountID: acctID,
|
|
||||||
Type: e,
|
|
||||||
Data: []byte(fmt.Sprintf(`{"eventType": %q}`, e)),
|
|
||||||
}); err != nil {
|
|
||||||
p.log.Errorf("failed to send sse event to listener: %v", err)
|
p.log.Errorf("failed to send sse event to listener: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ func (q *Queue) Send(ctx context.Context, e Event) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StandardEvent(acctID int64, eventType string) Event {
|
||||||
|
return Event{
|
||||||
|
AccountID: acctID,
|
||||||
|
Type: eventType,
|
||||||
|
Data: []byte(fmt.Sprintf(`{"eventType": %q}`, eventType)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Event) Write(w http.ResponseWriter) {
|
func (e *Event) Write(w http.ResponseWriter) {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
w,
|
w,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package sse
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type (
|
||||||
|
// sender is satisfied by *Queue
|
||||||
|
sender interface {
|
||||||
|
Send(ctx context.Context, e Event) error
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"ruben/inventory2/server/ui/svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// TODO: rename to BarChart
|
||||||
|
Bar struct {
|
||||||
|
values []BarValue
|
||||||
|
classes barChartClasses
|
||||||
|
}
|
||||||
|
|
||||||
|
BarValue struct {
|
||||||
|
Label string
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
barChartClasses struct {
|
||||||
|
svg []string
|
||||||
|
bar []string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewBar(vs ...BarValue) *Bar {
|
||||||
|
return &Bar{
|
||||||
|
values: vs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Bar) WithSVGClass(classes ...string) *Bar {
|
||||||
|
c.classes.svg = append(c.classes.svg, classes...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Bar) WithBarClass(classes ...string) *Bar {
|
||||||
|
c.classes.bar = append(c.classes.bar, classes...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Bar) SVG() svg.MarshalerReader {
|
||||||
|
const (
|
||||||
|
minX = 0
|
||||||
|
minY = 0
|
||||||
|
maxX = 100
|
||||||
|
maxY = 100
|
||||||
|
rangeX = maxX - minX
|
||||||
|
rangeY = maxY - minY
|
||||||
|
)
|
||||||
|
|
||||||
|
s := svg.NewSVG().
|
||||||
|
Attr(
|
||||||
|
svg.Style{
|
||||||
|
"width": "100%",
|
||||||
|
"height": "auto",
|
||||||
|
"padding": "1em",
|
||||||
|
"border-width": "2px",
|
||||||
|
},
|
||||||
|
svg.ViewBox{
|
||||||
|
X: minX,
|
||||||
|
Y: minY,
|
||||||
|
Width: maxX - minX,
|
||||||
|
Height: maxY - minY,
|
||||||
|
},
|
||||||
|
svg.Class(
|
||||||
|
strings.Join(append(
|
||||||
|
[]string{"rounded-lg", "border-border"},
|
||||||
|
c.classes.bar...,
|
||||||
|
), " "),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(c.classes.svg) != 0 {
|
||||||
|
s = s.Attr(svg.Class(strings.Join(c.classes.svg, " ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.values) > 0 {
|
||||||
|
minV := slices.MinFunc(c.values, func(a, b BarValue) int {
|
||||||
|
if a.Value < b.Value {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.Value > b.Value {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
maxV := slices.MaxFunc(c.values, func(a, b BarValue) int {
|
||||||
|
if a.Value < b.Value {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.Value > b.Value {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
rangeV := maxV.Value - minV.Value
|
||||||
|
|
||||||
|
spacePerBar := float64(rangeX) / float64(len(c.values))
|
||||||
|
barPadding := 0.025 * spacePerBar
|
||||||
|
barWidth := spacePerBar - (2 * barPadding)
|
||||||
|
fontSize := barWidth / 2
|
||||||
|
|
||||||
|
for i, v := range c.values {
|
||||||
|
scaledV := (v.Value - minV.Value) * rangeY / rangeV
|
||||||
|
barX := float64(i)*spacePerBar + barPadding
|
||||||
|
s = s.Child(
|
||||||
|
new(svg.Rect).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(barX).AsX(),
|
||||||
|
svg.NewLength(maxY-scaledV).AsY(),
|
||||||
|
svg.NewLength(barWidth).AsWidth(),
|
||||||
|
svg.NewLength(scaledV).AsHeight(),
|
||||||
|
svg.Class(
|
||||||
|
strings.Join(append(
|
||||||
|
[]string{"fill-accent"},
|
||||||
|
c.classes.bar...,
|
||||||
|
), " "),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
svg.NewText(fmt.Sprint(v.Value)).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(barX+(barWidth/2)).AsX(),
|
||||||
|
svg.NewLength(maxY-scaledV+fontSize).AsY(),
|
||||||
|
svg.Fill("var(--accent-foreground)"),
|
||||||
|
svg.TextAnchorMiddle,
|
||||||
|
// svg.NewLength(fontSize).AsFontSize(),
|
||||||
|
svg.Style{
|
||||||
|
"font-size": fmt.Sprintf("%vpx", fontSize),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if v.Label != "" {
|
||||||
|
x := barX + (barWidth / 2)
|
||||||
|
y := maxY - fontSize
|
||||||
|
s = s.Child(
|
||||||
|
svg.NewText(v.Label).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(x).AsX(),
|
||||||
|
svg.NewLength(y).AsY(),
|
||||||
|
svg.Fill("var(--accent-foreground)"),
|
||||||
|
//svg.NewLength(fontSize).AsFontSize(),
|
||||||
|
svg.Style{
|
||||||
|
"font-size": fmt.Sprintf("%vpx", fontSize),
|
||||||
|
},
|
||||||
|
svg.DominantBaselineCentral,
|
||||||
|
svg.TransformRotate(-90).About(x, y),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleBar() {
|
||||||
|
mu := NewBar(
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 101",
|
||||||
|
Value: 101,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -09",
|
||||||
|
Value: -99,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 201",
|
||||||
|
Value: 201,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -199",
|
||||||
|
Value: -199,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 301",
|
||||||
|
Value: 301,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -299",
|
||||||
|
Value: -299,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 401",
|
||||||
|
Value: 401,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -399",
|
||||||
|
Value: -399,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 501",
|
||||||
|
Value: 501,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -499",
|
||||||
|
Value: -499,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 601",
|
||||||
|
Value: 601,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -599",
|
||||||
|
Value: -599,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 701",
|
||||||
|
Value: 701,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -699",
|
||||||
|
Value: -699,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 801",
|
||||||
|
Value: 801,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -799",
|
||||||
|
Value: -799,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 901",
|
||||||
|
Value: 901,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -899",
|
||||||
|
Value: -899,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: 1001",
|
||||||
|
Value: 1001,
|
||||||
|
},
|
||||||
|
BarValue{
|
||||||
|
Label: "label: -999",
|
||||||
|
Value: -999,
|
||||||
|
},
|
||||||
|
).SVG().GetMarkup()
|
||||||
|
|
||||||
|
os.WriteFile("./test.svg", []byte(mu), 666)
|
||||||
|
fmt.Println(mu)
|
||||||
|
// Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"><rect x="0" y="45" width="4.75" height="55" class="fill-accent"/><rect x="5" y="55" width="4.75" height="45" class="fill-accent"/><rect x="10" y="40" width="4.75" height="60" class="fill-accent"/><rect x="15" y="60" width="4.75" height="40" class="fill-accent"/><rect x="20" y="35" width="4.75" height="65" class="fill-accent"/><rect x="25" y="65" width="4.75" height="35" class="fill-accent"/><rect x="30" y="30" width="4.75" height="70" class="fill-accent"/><rect x="35" y="70" width="4.75" height="30" class="fill-accent"/><rect x="40" y="25" width="4.75" height="75" class="fill-accent"/><rect x="45" y="75" width="4.75" height="25" class="fill-accent"/><rect x="50" y="20" width="4.75" height="80" class="fill-accent"/><rect x="55" y="80" width="4.75" height="20" class="fill-accent"/><rect x="60" y="15" width="4.75" height="85" class="fill-accent"/><rect x="65" y="85" width="4.75" height="15" class="fill-accent"/><rect x="70" y="10" width="4.75" height="90" class="fill-accent"/><rect x="75" y="90" width="4.75" height="10" class="fill-accent"/><rect x="80" y="5" width="4.75" height="95" class="fill-accent"/><rect x="85" y="95" width="4.75" height="5" class="fill-accent"/><rect x="90" y="0" width="4.75" height="100" class="fill-accent"/><rect x="95" y="100" width="4.75" height="0" class="fill-accent"/></svg>
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ruben/inventory2/server/ui/svg"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
LineChart struct {
|
||||||
|
width float64
|
||||||
|
height float64
|
||||||
|
foreground string
|
||||||
|
background string
|
||||||
|
values []LineValue
|
||||||
|
max *float64
|
||||||
|
min *float64
|
||||||
|
}
|
||||||
|
LineValue struct {
|
||||||
|
Label string
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLineChart(width, height float64, vs ...LineValue) *LineChart {
|
||||||
|
return &LineChart{
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
values: vs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LineChart) Max(n float64) *LineChart {
|
||||||
|
c.max = &n
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LineChart) Min(n float64) *LineChart {
|
||||||
|
c.min = &n
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LineChart) Foreground(fg string) *LineChart {
|
||||||
|
c.foreground = fg
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LineChart) Background(bg string) *LineChart {
|
||||||
|
c.background = bg
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LineChart) SVG() svg.MarshalerReader {
|
||||||
|
var (
|
||||||
|
minVal float64
|
||||||
|
maxVal float64
|
||||||
|
)
|
||||||
|
|
||||||
|
fg := "black"
|
||||||
|
if c.foreground != "" {
|
||||||
|
fg = c.foreground
|
||||||
|
}
|
||||||
|
bg := c.background
|
||||||
|
|
||||||
|
if c.min != nil {
|
||||||
|
minVal = *c.min
|
||||||
|
} else if len(c.values) > 0 {
|
||||||
|
minVal = slices.MinFunc(c.values, compareLineValues).Value
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.max != nil {
|
||||||
|
maxVal = *c.max
|
||||||
|
} else if len(c.values) > 0 {
|
||||||
|
maxVal = slices.MaxFunc(c.values, compareLineValues).Value
|
||||||
|
}
|
||||||
|
|
||||||
|
svgBg := bg
|
||||||
|
if svgBg == "" {
|
||||||
|
svgBg = "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
s := svg.NewSVG().
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(c.width).
|
||||||
|
AsWidth(),
|
||||||
|
svg.NewLength(c.height).
|
||||||
|
AsHeight(),
|
||||||
|
svg.Style{
|
||||||
|
// "width": "100%",
|
||||||
|
// "height": "100%",
|
||||||
|
"background": svgBg,
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
svg.ViewBox{
|
||||||
|
X: 0,
|
||||||
|
Y: minVal,
|
||||||
|
Width: float64(len(c.values) - 1),
|
||||||
|
Height: maxVal - minVal,
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
svg.ViewBox{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
Width: c.width,
|
||||||
|
Height: c.height,
|
||||||
|
},
|
||||||
|
svg.PreserveAspectRatio{},
|
||||||
|
)
|
||||||
|
|
||||||
|
g := new(svg.G).
|
||||||
|
Attr(
|
||||||
|
// getChartOrientationTransform(c.height),
|
||||||
|
svg.Transform{
|
||||||
|
// scale down to provide padding
|
||||||
|
svg.TransformTranslate{
|
||||||
|
X: 0.05 * c.width,
|
||||||
|
Y: 0.05 * c.height,
|
||||||
|
},
|
||||||
|
svg.TransformScale{
|
||||||
|
X: 0.9,
|
||||||
|
Y: 0.9,
|
||||||
|
},
|
||||||
|
|
||||||
|
// flip
|
||||||
|
svg.TransformScale{
|
||||||
|
X: 1,
|
||||||
|
Y: -1,
|
||||||
|
},
|
||||||
|
svg.TransformTranslate{
|
||||||
|
Y: -c.height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// lines
|
||||||
|
|
||||||
|
pts := make(svg.Points, len(c.values))
|
||||||
|
for i, p := range c.values {
|
||||||
|
pts[i] = svg.Point{
|
||||||
|
X: (c.width / float64(len(c.values)-1)) * float64(i),
|
||||||
|
Y: ((p.Value - minVal) * c.height) / (maxVal - minVal),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g = g.Child(
|
||||||
|
new(svg.Polyline).Attr(
|
||||||
|
pts,
|
||||||
|
svg.NewLength(2).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsStrokeWidth(),
|
||||||
|
svg.VectorEffectNonScalingStroke,
|
||||||
|
svg.Stroke(fg),
|
||||||
|
svg.Fill("none"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// points
|
||||||
|
|
||||||
|
if len(c.values) > 0 {
|
||||||
|
r := 1.0 / float64(len(c.values))
|
||||||
|
rx := r
|
||||||
|
ry := r
|
||||||
|
|
||||||
|
for i, p := range c.values {
|
||||||
|
// dot on the line graph
|
||||||
|
|
||||||
|
x := pts[i].X
|
||||||
|
y := pts[i].Y
|
||||||
|
|
||||||
|
ell := new(svg.Ellipse).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(x).
|
||||||
|
AsCX(),
|
||||||
|
svg.NewLength(y).
|
||||||
|
AsCY(),
|
||||||
|
svg.Percentage(rx).
|
||||||
|
AsRX(),
|
||||||
|
svg.Percentage(ry).
|
||||||
|
AsRY(),
|
||||||
|
svg.Stroke(fg),
|
||||||
|
svg.NewLength(2).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsStrokeWidth(),
|
||||||
|
svg.VectorEffectNonScalingStroke,
|
||||||
|
)
|
||||||
|
|
||||||
|
if bg != "" {
|
||||||
|
ell = ell.Attr(
|
||||||
|
svg.Fill(bg),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ell = ell.Attr(
|
||||||
|
svg.Fill(fg),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
g = g.Child(
|
||||||
|
ell,
|
||||||
|
|
||||||
|
// label
|
||||||
|
upsideDownCenteredText(p.Label, x, y).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(c.height*0.05).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsFontSize(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Child(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareLineValues(a, b LineValue) int {
|
||||||
|
if a.Value < b.Value {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.Value > b.Value {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsideDownCenteredText(txt string, x, y float64) *svg.Text {
|
||||||
|
return svg.NewText(txt).
|
||||||
|
Attr(
|
||||||
|
svg.TextAnchorMiddle,
|
||||||
|
svg.DominantBaselineMiddle,
|
||||||
|
|
||||||
|
svg.NewLength(x).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(y).
|
||||||
|
AsY(),
|
||||||
|
|
||||||
|
upsideDownTransform(x, y),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleLineChart() {
|
||||||
|
c := NewLineChart(100, 100,
|
||||||
|
LineValue{
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
LineValue{
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
LineValue{
|
||||||
|
Value: 4,
|
||||||
|
},
|
||||||
|
LineValue{
|
||||||
|
Value: 8,
|
||||||
|
},
|
||||||
|
LineValue{
|
||||||
|
Value: 16,
|
||||||
|
},
|
||||||
|
LineValue{
|
||||||
|
Value: 32,
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Min(0).
|
||||||
|
Max(40).
|
||||||
|
Foreground("purple").
|
||||||
|
Background("pink").
|
||||||
|
SVG().
|
||||||
|
GetMarkup()
|
||||||
|
|
||||||
|
output := []byte(`<html><body>` + c + `</body></html>`)
|
||||||
|
os.WriteFile("./line_chart.html", []byte(output), 0666)
|
||||||
|
fmt.Println(c)
|
||||||
|
// Output: <svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"></svg>
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import "ruben/inventory2/server/ui/svg"
|
||||||
|
|
||||||
|
func getChartOrientationTransform(height float64) svg.Transform {
|
||||||
|
return svg.Transform{
|
||||||
|
svg.TransformScale{
|
||||||
|
X: 1,
|
||||||
|
Y: -1,
|
||||||
|
},
|
||||||
|
svg.TransformTranslate{
|
||||||
|
Y: -height,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsideDownTransform(x, y float64) svg.Transform {
|
||||||
|
return svg.Transform{
|
||||||
|
svg.TransformTranslate{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
},
|
||||||
|
svg.TransformScale{
|
||||||
|
X: 1,
|
||||||
|
Y: -1,
|
||||||
|
},
|
||||||
|
svg.TransformTranslate{
|
||||||
|
X: -x,
|
||||||
|
Y: -y,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="height: auto; width: 100%" viewBox="0 0 100 100"><rect x="0" y="45" width="4.75" height="55" class="fill-accent"/><rect x="5" y="55" width="4.75" height="45" class="fill-accent"/><rect x="10" y="40" width="4.75" height="60" class="fill-accent"/><rect x="15" y="60" width="4.75" height="40" class="fill-accent"/><rect x="20" y="35" width="4.75" height="65" class="fill-accent"/><rect x="25" y="65" width="4.75" height="35" class="fill-accent"/><rect x="30" y="30" width="4.75" height="70" class="fill-accent"/><rect x="35" y="70" width="4.75" height="30" class="fill-accent"/><rect x="40" y="25" width="4.75" height="75" class="fill-accent"/><rect x="45" y="75" width="4.75" height="25" class="fill-accent"/><rect x="50" y="20" width="4.75" height="80" class="fill-accent"/><rect x="55" y="80" width="4.75" height="20" class="fill-accent"/><rect x="60" y="15" width="4.75" height="85" class="fill-accent"/><rect x="65" y="85" width="4.75" height="15" class="fill-accent"/><rect x="70" y="10" width="4.75" height="90" class="fill-accent"/><rect x="75" y="90" width="4.75" height="10" class="fill-accent"/><rect x="80" y="5" width="4.75" height="95" class="fill-accent"/><rect x="85" y="95" width="4.75" height="5" class="fill-accent"/><rect x="90" y="0" width="4.75" height="100" class="fill-accent"/><rect x="95" y="100" width="4.75" height="0" class="fill-accent"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,425 @@
|
|||||||
|
package charts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ruben/inventory2/server/ui/svg"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TimeLineChart struct {
|
||||||
|
width float64
|
||||||
|
height float64
|
||||||
|
foreground string
|
||||||
|
background string
|
||||||
|
values []TimeLineValue
|
||||||
|
max *float64
|
||||||
|
min *float64
|
||||||
|
}
|
||||||
|
TimeLineValue struct {
|
||||||
|
Label string
|
||||||
|
Time time.Time
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTimeLineChart(width, height float64, vs ...TimeLineValue) *TimeLineChart {
|
||||||
|
return &TimeLineChart{
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
values: validValues(vs),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validValues(vs []TimeLineValue) []TimeLineValue {
|
||||||
|
values := make([]TimeLineValue, 0, len(vs))
|
||||||
|
for _, v := range vs {
|
||||||
|
if !v.Time.IsZero() {
|
||||||
|
values = append(values, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) Max(n float64) *TimeLineChart {
|
||||||
|
c.max = &n
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) Min(n float64) *TimeLineChart {
|
||||||
|
c.min = &n
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) Foreground(fg string) *TimeLineChart {
|
||||||
|
c.foreground = fg
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) Background(bg string) *TimeLineChart {
|
||||||
|
c.background = bg
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) SVG() svg.MarshalerReader {
|
||||||
|
return svg.NewSVG().
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(c.width).
|
||||||
|
AsWidth(),
|
||||||
|
svg.NewLength(c.height).
|
||||||
|
AsHeight(),
|
||||||
|
svg.Style{
|
||||||
|
"background": c.getOuterSVGBackground(),
|
||||||
|
},
|
||||||
|
svg.ViewBox{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
Width: c.width,
|
||||||
|
Height: c.height,
|
||||||
|
},
|
||||||
|
svg.PreserveAspectRatio{},
|
||||||
|
).
|
||||||
|
Child(
|
||||||
|
new(svg.G).
|
||||||
|
Attr(
|
||||||
|
svg.Transform{
|
||||||
|
// scale down to provide padding
|
||||||
|
svg.TransformTranslate{
|
||||||
|
X: 0.05 * c.width,
|
||||||
|
Y: 0.05 * c.height,
|
||||||
|
},
|
||||||
|
svg.TransformScale{
|
||||||
|
X: 0.9,
|
||||||
|
Y: 0.85, // a little more padding on the bottom
|
||||||
|
},
|
||||||
|
|
||||||
|
// flip
|
||||||
|
svg.TransformScale{
|
||||||
|
X: 1,
|
||||||
|
Y: -1,
|
||||||
|
},
|
||||||
|
svg.TransformTranslate{
|
||||||
|
Y: -c.height,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Child(c.getChildren()...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getOuterSVGBackground() string {
|
||||||
|
if c.background != "" {
|
||||||
|
return c.background
|
||||||
|
}
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getChildren() []svg.GChildren {
|
||||||
|
pts := c.getPoints()
|
||||||
|
|
||||||
|
return append(
|
||||||
|
append(
|
||||||
|
[]svg.GChildren{
|
||||||
|
new(svg.Polyline).Attr(
|
||||||
|
pts,
|
||||||
|
svg.NewLength(2).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsStrokeWidth(),
|
||||||
|
svg.VectorEffectNonScalingStroke,
|
||||||
|
svg.Stroke(c.getStroke()),
|
||||||
|
svg.Fill("none"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
c.buildDots(pts)...,
|
||||||
|
),
|
||||||
|
c.buildLabels(pts)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getPoints() svg.Points {
|
||||||
|
minVal, _ := c.getMinVal()
|
||||||
|
maxVal, _ := c.getMaxVal()
|
||||||
|
|
||||||
|
minTime, maxTime := c.getMinAndMaxUnixTimes()
|
||||||
|
|
||||||
|
pts := make(svg.Points, len(c.values))
|
||||||
|
for i, p := range c.values {
|
||||||
|
pts[i] = svg.Point{
|
||||||
|
X: c.width * float64(p.Time.UnixNano()-minTime) / float64(maxTime-minTime),
|
||||||
|
Y: ((p.Value - minVal) * c.height) / (maxVal - minVal),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getMinVal() (float64, bool) {
|
||||||
|
if c.min != nil {
|
||||||
|
return *c.min, true
|
||||||
|
}
|
||||||
|
if values := c.values; len(values) > 0 {
|
||||||
|
return slices.MinFunc(values, compareTimeLineValues).Value, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getMaxVal() (float64, bool) {
|
||||||
|
if c.max != nil {
|
||||||
|
return *c.max, true
|
||||||
|
}
|
||||||
|
if len(c.values) > 0 {
|
||||||
|
return slices.MaxFunc(c.values, compareTimeLineValues).Value, true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareTimeLineValues(a, b TimeLineValue) int {
|
||||||
|
if a.Value < b.Value {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.Value > b.Value {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getMinAndMaxUnixTimes() (minTime, maxTime int64) {
|
||||||
|
if len(c.values) > 0 {
|
||||||
|
return slices.MinFunc(c.values, compareTimeLineTimeValues).Time.UnixNano(),
|
||||||
|
slices.MaxFunc(c.values, compareTimeLineTimeValues).Time.UnixNano()
|
||||||
|
}
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareTimeLineTimeValues(a, b TimeLineValue) int {
|
||||||
|
at := a.Time
|
||||||
|
bt := b.Time
|
||||||
|
an := at.UnixNano()
|
||||||
|
bn := bt.UnixNano()
|
||||||
|
if at.IsZero() {
|
||||||
|
an = 0
|
||||||
|
}
|
||||||
|
if bt.IsZero() {
|
||||||
|
bn = 0
|
||||||
|
}
|
||||||
|
return int(an - bn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildDots(pts svg.Points) []svg.GChildren {
|
||||||
|
if len(c.values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chn := make([]svg.GChildren, len(c.values))
|
||||||
|
for i, p := range pts {
|
||||||
|
chn[i] = c.buildDot(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildDot(p svg.Point) *svg.Circle {
|
||||||
|
return new(svg.Circle).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(p.X).
|
||||||
|
AsCX(),
|
||||||
|
svg.NewLength(p.Y).
|
||||||
|
AsCY(),
|
||||||
|
svg.NewLength(0.01*min(c.width, c.height)).
|
||||||
|
AsR(),
|
||||||
|
svg.Stroke(c.getStroke()),
|
||||||
|
svg.NewLength(2).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsStrokeWidth(),
|
||||||
|
svg.VectorEffectNonScalingStroke,
|
||||||
|
c.getDotFill(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getDotFill() svg.Fill {
|
||||||
|
if c.background != "" {
|
||||||
|
return svg.Fill(c.background)
|
||||||
|
} else {
|
||||||
|
return svg.Fill(c.getStroke())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildLabels(pts svg.Points) []svg.GChildren {
|
||||||
|
if len(c.values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chn := make([]svg.GChildren, len(c.values))
|
||||||
|
for i, p := range pts {
|
||||||
|
chn[i] = c.buildLabel(c.values[i], p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildLabel(v TimeLineValue, p svg.Point) *svg.Element[svg.SVGTag, svg.SVGAttribute, svg.SVGChildren] {
|
||||||
|
return svg.NewSVG().
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(p.X).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(p.Y).
|
||||||
|
AsY(),
|
||||||
|
svg.NewLength(24).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsWidth(),
|
||||||
|
svg.NewLength(24).
|
||||||
|
Unit(svg.Px).
|
||||||
|
AsHeight(),
|
||||||
|
svg.Class("group"),
|
||||||
|
svg.Style{
|
||||||
|
"overflow": "visible",
|
||||||
|
"fill": "var(--accent-foreground)",
|
||||||
|
},
|
||||||
|
svg.ViewBox{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
Width: 1,
|
||||||
|
Height: 1,
|
||||||
|
},
|
||||||
|
svg.PreserveAspectRatio{
|
||||||
|
Align: &svg.AlignValue{
|
||||||
|
X: svg.AlignMid,
|
||||||
|
Y: svg.AlignMid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Child(
|
||||||
|
c.buildAlwaysDisplayedLabelText(v, p),
|
||||||
|
new(svg.Rect).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(0.1).
|
||||||
|
AsStrokeWidth(),
|
||||||
|
svg.Fill("var(--muted)"),
|
||||||
|
svg.Stroke("var(--accent-foreground)"),
|
||||||
|
svg.NewLength(2.5).
|
||||||
|
AsHeight(),
|
||||||
|
svg.NewLength(-5).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(-3).
|
||||||
|
AsY(),
|
||||||
|
svg.NewLength(10).
|
||||||
|
AsWidth(),
|
||||||
|
svg.Class("not-group-hover:hidden"),
|
||||||
|
),
|
||||||
|
c.buildHoverLabelText(v, p),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildAlwaysDisplayedLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||||
|
return c.buildLabelText(v, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildHoverLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||||
|
labelText := c.buildLabelText(v, p).
|
||||||
|
Attr(
|
||||||
|
svg.Class("not-group-hover:hidden"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if v.Time.IsZero() {
|
||||||
|
return labelText
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelText.Child(
|
||||||
|
svg.NewText(v.Time.Format("Jan _2 3:04:05 PM")).
|
||||||
|
Attr(
|
||||||
|
svg.TextAnchorMiddle,
|
||||||
|
svg.DominantBaselineMiddle,
|
||||||
|
svg.Style{
|
||||||
|
"font-size": "1px",
|
||||||
|
},
|
||||||
|
svg.Class("not-group-hover:hidden"),
|
||||||
|
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(2.5).
|
||||||
|
AsY(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TimeLineChart) buildLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||||
|
return new(svg.G).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsY(),
|
||||||
|
upsideDownTransform(0, 0),
|
||||||
|
).
|
||||||
|
Child(
|
||||||
|
svg.NewText(v.Label).
|
||||||
|
Attr(
|
||||||
|
svg.TextAnchorMiddle,
|
||||||
|
svg.DominantBaselineMiddle,
|
||||||
|
svg.Style{
|
||||||
|
"font-size": "1px",
|
||||||
|
},
|
||||||
|
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(1.25).
|
||||||
|
AsY(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (c *TimeLineChart) buildLabelText(v TimeLineValue, p svg.Point) *svg.Element[svg.GTag, svg.GAttribute, svg.GChildren] {
|
||||||
|
labelText := new(svg.G).
|
||||||
|
Attr(
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsY(),
|
||||||
|
upsideDownTransform(0, 0),
|
||||||
|
).
|
||||||
|
Child(
|
||||||
|
svg.NewText(v.Label).
|
||||||
|
Attr(
|
||||||
|
svg.TextAnchorMiddle,
|
||||||
|
svg.DominantBaselineMiddle,
|
||||||
|
svg.Style{
|
||||||
|
"font-size": "1px",
|
||||||
|
},
|
||||||
|
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(1.25).
|
||||||
|
AsY(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if v.Time.IsZero() {
|
||||||
|
return labelText
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelText.Child(
|
||||||
|
svg.NewText(v.Time.Format("Jan _2 3:04:05 PM")).
|
||||||
|
Attr(
|
||||||
|
svg.TextAnchorMiddle,
|
||||||
|
svg.DominantBaselineMiddle,
|
||||||
|
svg.Style{
|
||||||
|
"font-size": "1px",
|
||||||
|
},
|
||||||
|
svg.Class("not-group-hover:hidden"),
|
||||||
|
|
||||||
|
svg.NewLength(0).
|
||||||
|
AsX(),
|
||||||
|
svg.NewLength(2.5).
|
||||||
|
AsY(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (c *TimeLineChart) getStroke() string {
|
||||||
|
if c.foreground != "" {
|
||||||
|
return c.foreground
|
||||||
|
}
|
||||||
|
return "black"
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"ruben/inventory2/domains/accounts"
|
"ruben/inventory2/domains/accounts"
|
||||||
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
etsy_platform "ruben/inventory2/domains/platforms/etsy"
|
||||||
"ruben/inventory2/domains/raw_events"
|
"ruben/inventory2/domains/raw_events"
|
||||||
|
"ruben/inventory2/domains/reports"
|
||||||
"ruben/inventory2/logging"
|
"ruben/inventory2/logging"
|
||||||
"ruben/inventory2/server/auth"
|
"ruben/inventory2/server/auth"
|
||||||
"ruben/inventory2/server/response"
|
"ruben/inventory2/server/response"
|
||||||
@@ -28,6 +29,7 @@ type (
|
|||||||
templater *templater.Templater
|
templater *templater.Templater
|
||||||
rawEvents *raw_events.Store
|
rawEvents *raw_events.Store
|
||||||
accts *accounts.Store
|
accts *accounts.Store
|
||||||
|
reports *reports.Store
|
||||||
etsy *etsy_platform.Platform
|
etsy *etsy_platform.Platform
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +42,11 @@ type (
|
|||||||
|
|
||||||
func Routes(
|
func Routes(
|
||||||
logger *logging.Logger,
|
logger *logging.Logger,
|
||||||
r gin.IRoutes,
|
r gin.IRouter,
|
||||||
uiPath string,
|
uiPath string,
|
||||||
rawEvents *raw_events.Store,
|
rawEvents *raw_events.Store,
|
||||||
accts *accounts.Store,
|
accts *accounts.Store,
|
||||||
|
reps *reports.Store,
|
||||||
etsy *etsy_platform.Platform,
|
etsy *etsy_platform.Platform,
|
||||||
authenticate gin.HandlerFunc,
|
authenticate gin.HandlerFunc,
|
||||||
) {
|
) {
|
||||||
@@ -98,6 +101,39 @@ func Routes(
|
|||||||
"multInt": func(a, b int) int {
|
"multInt": func(a, b int) int {
|
||||||
return a * b
|
return a * b
|
||||||
},
|
},
|
||||||
|
"divInt": func(a, b int) int {
|
||||||
|
return a / b
|
||||||
|
},
|
||||||
|
"intToFloat": func(n int) float64 {
|
||||||
|
return float64(n)
|
||||||
|
},
|
||||||
|
"addInt64": func(a, b int64) int64 {
|
||||||
|
return a + b
|
||||||
|
},
|
||||||
|
"subInt64": func(a, b int64) int64 {
|
||||||
|
return a - b
|
||||||
|
},
|
||||||
|
"multInt64": func(a, b int64) int64 {
|
||||||
|
return a * b
|
||||||
|
},
|
||||||
|
"divInt64": func(a, b int64) int64 {
|
||||||
|
return a / b
|
||||||
|
},
|
||||||
|
"int64ToFloat": func(n int64) float64 {
|
||||||
|
return float64(n)
|
||||||
|
},
|
||||||
|
"addFloat": func(a, b float64) float64 {
|
||||||
|
return a + b
|
||||||
|
},
|
||||||
|
"subFloat": func(a, b float64) float64 {
|
||||||
|
return a - b
|
||||||
|
},
|
||||||
|
"multFloat": func(a, b float64) float64 {
|
||||||
|
return a * b
|
||||||
|
},
|
||||||
|
"divFloat": func(a, b float64) float64 {
|
||||||
|
return a / b
|
||||||
|
},
|
||||||
|
|
||||||
// html
|
// html
|
||||||
"rawHTML": func(s string) template.HTML {
|
"rawHTML": func(s string) template.HTML {
|
||||||
@@ -143,6 +179,7 @@ func Routes(
|
|||||||
}),
|
}),
|
||||||
rawEvents: rawEvents,
|
rawEvents: rawEvents,
|
||||||
accts: accts,
|
accts: accts,
|
||||||
|
reports: reps,
|
||||||
etsy: etsy,
|
etsy: etsy,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +207,14 @@ func (s *webpageRouter) serveTemplate(c *gin.Context) (response.Response, error)
|
|||||||
|
|
||||||
r := c.Request
|
r := c.Request
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
id := auth.GetIdentity(ctx)
|
||||||
|
var mockMode bool
|
||||||
|
if id.Account != nil {
|
||||||
|
var err error
|
||||||
|
if mockMode, err = s.accts.GetMockMode(ctx, id.Account.AccountID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args := []any{
|
args := []any{
|
||||||
"Request",
|
"Request",
|
||||||
@@ -181,12 +226,16 @@ func (s *webpageRouter) serveTemplate(c *gin.Context) (response.Response, error)
|
|||||||
newURLCalculator(r.URL),
|
newURLCalculator(r.URL),
|
||||||
"Accounts",
|
"Accounts",
|
||||||
s.accts.WithContext(ctx),
|
s.accts.WithContext(ctx),
|
||||||
|
"Reports",
|
||||||
|
s.reports.WithContext(ctx),
|
||||||
"Etsy",
|
"Etsy",
|
||||||
s.etsy.WithContext(ctx),
|
s.etsy.WithContext(ctx),
|
||||||
|
"MockMode",
|
||||||
|
mockMode,
|
||||||
|
|
||||||
// auth tooling
|
// auth tooling
|
||||||
"Identity",
|
"Identity",
|
||||||
auth.GetIdentity(ctx),
|
id,
|
||||||
"Auth",
|
"Auth",
|
||||||
newTemplateAuthenticator(r),
|
newTemplateAuthenticator(r),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Attribute is the minimum method set of of an svg attribute.
|
||||||
|
Attribute interface {
|
||||||
|
// PrintKey must return the svg attribute key
|
||||||
|
PrintKey() string
|
||||||
|
// PrintVAlue must return the svg attribute value string, if applicable
|
||||||
|
PrintValue() (string, bool)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintAttribute(a Attribute) string {
|
||||||
|
v, ok := a.PrintValue()
|
||||||
|
if !ok {
|
||||||
|
return a.PrintKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s=%s", a.PrintKey(), v)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Circle = VoidElement[CircleTag, CircleAttribute]
|
||||||
|
|
||||||
|
CircleAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsCircleAttribute()
|
||||||
|
}
|
||||||
|
|
||||||
|
CircleTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (CircleTag) PrintTag() string {
|
||||||
|
return "circle"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CX) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CXP) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CY) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CYP) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (R) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RP) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Fill) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Stroke) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StrokeWidth) IsCircleAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VectorEffect) IsCircleAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Class string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (Class) PrintKey() string {
|
||||||
|
return "class"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Class) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf(`"%s"`, html.EscapeString(string(c))), true
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
CX struct {
|
||||||
|
*LengthAttr[CXTag]
|
||||||
|
}
|
||||||
|
CXP struct {
|
||||||
|
PercentageAttr[CXTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
CXTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsCX() CX {
|
||||||
|
return CX{LengthAttr: (*LengthAttr[CXTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsCX() CXP {
|
||||||
|
return CXP{PercentageAttr: (PercentageAttr[CXTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CXTag) PrintTag() string {
|
||||||
|
return "cx"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
CY struct {
|
||||||
|
*LengthAttr[CYTag]
|
||||||
|
}
|
||||||
|
CYP struct {
|
||||||
|
PercentageAttr[CYTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
CYTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsCY() CY {
|
||||||
|
return CY{LengthAttr: (*LengthAttr[CYTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsCY() CYP {
|
||||||
|
return CYP{PercentageAttr: (PercentageAttr[CYTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CYTag) PrintTag() string {
|
||||||
|
return "cy"
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
D []PathCommand
|
||||||
|
|
||||||
|
PathCommand interface {
|
||||||
|
PrintPathCommand() (string, string, bool)
|
||||||
|
PrintPathCommandParameters() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathCommand implementations
|
||||||
|
|
||||||
|
// PathRelative makes any PathCommand and converts it into a relative command (makes lowercase)
|
||||||
|
PathRelative[C PathCommand] struct {
|
||||||
|
Command C
|
||||||
|
}
|
||||||
|
|
||||||
|
PathMoveTo struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
PathLineTo struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
PathHorizontalLine float64
|
||||||
|
PathVerticalLine float64
|
||||||
|
|
||||||
|
PathCubicBezier []PathCubicBezierParameterTuple
|
||||||
|
PathCubicBezierParameterTuple struct {
|
||||||
|
X1, Y1 float64
|
||||||
|
X2, Y2 float64
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
PathSmoothCubicBezier []PathSmoothCubicBezierParameterTuple
|
||||||
|
PathSmoothCubicBezierParameterTuple struct {
|
||||||
|
X2, Y2 float64
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
PathQuadraticBezier []PathQuadraticBezierParameterTuple
|
||||||
|
PathQuadraticBezierParameterTuple struct {
|
||||||
|
X1, Y1 float64
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
PathSmoothQuadraticBezier []PathSmoothQuadraticBezierParameterTuple
|
||||||
|
PathSmoothQuadraticBezierParameterTuple struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
PathElliptical []PathEllipticalParameterTuple
|
||||||
|
PathEllipticalParameterTuple struct {
|
||||||
|
RX, RY float64
|
||||||
|
Angle float64
|
||||||
|
LargeArc bool
|
||||||
|
Clockwise bool
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
PathClose struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrintPathCommand(c PathCommand) (string, bool) {
|
||||||
|
cmd, params, ok := c.PrintPathCommand()
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", cmd, params), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathRelative[C]) PrintPathCommand() (string, string, bool) {
|
||||||
|
cmd, params, ok := c.Command.PrintPathCommand()
|
||||||
|
return strings.ToLower(cmd), params, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathMoveTo) PrintPathCommand() (string, string, bool) {
|
||||||
|
return "M", fmt.Sprintf("%v,%v", c.X, c.Y), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathLineTo) PrintPathCommand() (string, string, bool) {
|
||||||
|
return "L", fmt.Sprintf("%v,%v", c.X, c.Y), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathHorizontalLine) PrintPathCommand() (string, string, bool) {
|
||||||
|
return "H", fmt.Sprintf("%v", float64(c)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathVerticalLine) PrintPathCommand() (string, string, bool) {
|
||||||
|
return "V", fmt.Sprintf("%v", float64(c)), false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathCubicBezier) PrintPathCommand() (string, string, bool) {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return "C", printTuples(c), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathCubicBezier) Append(x1, y1, x2, y2, x, y float64) PathCubicBezier {
|
||||||
|
return append(c, PathCubicBezierParameterTuple{
|
||||||
|
X1: x1,
|
||||||
|
Y1: y1,
|
||||||
|
X2: x2,
|
||||||
|
Y2: y2,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PathCubicBezierParameterTuple) String() string {
|
||||||
|
return fmt.Sprintf("%v,%v %v,%v %v,%v", t.X1, t.Y1, t.X2, t.Y2, t.X, t.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathSmoothCubicBezier) PrintPathCommand() (string, string, bool) {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return "S", printTuples(c), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathSmoothCubicBezier) Append(x2, y2, x, y float64) PathSmoothCubicBezier {
|
||||||
|
return append(c, PathSmoothCubicBezierParameterTuple{
|
||||||
|
X2: x2,
|
||||||
|
Y2: y2,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PathSmoothCubicBezierParameterTuple) String() string {
|
||||||
|
return fmt.Sprintf("%v,%v %v,%v", t.X2, t.Y2, t.X, t.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathQuadraticBezier) PrintPathCommand() (string, string, bool) {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return "Q", printTuples(c), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathQuadraticBezier) Append(x1, y1, x, y float64) PathQuadraticBezier {
|
||||||
|
return append(c, PathQuadraticBezierParameterTuple{
|
||||||
|
X1: x1,
|
||||||
|
Y1: y1,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PathQuadraticBezierParameterTuple) String() string {
|
||||||
|
return fmt.Sprintf("%v,%v %v,%v", t.X1, t.Y1, t.X, t.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathSmoothQuadraticBezier) PrintPathCommand() (string, string, bool) {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return "T", printTuples(c), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathSmoothQuadraticBezier) Append(x, y float64) PathSmoothQuadraticBezier {
|
||||||
|
return append(c, PathSmoothQuadraticBezierParameterTuple{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PathSmoothQuadraticBezierParameterTuple) String() string {
|
||||||
|
return fmt.Sprintf("%v,%v", t.X, t.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathElliptical) PrintPathCommand() (string, string, bool) {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return "A", printTuples(c), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c PathElliptical) Append(rx, ry, angle float64, largeArc, clockwise bool, x, y float64) PathElliptical {
|
||||||
|
return append(c, PathEllipticalParameterTuple{
|
||||||
|
RX: rx,
|
||||||
|
RY: ry,
|
||||||
|
Angle: angle,
|
||||||
|
LargeArc: largeArc,
|
||||||
|
Clockwise: clockwise,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t PathEllipticalParameterTuple) String() string {
|
||||||
|
var (
|
||||||
|
largeArcFlag int
|
||||||
|
sweepFlag int
|
||||||
|
)
|
||||||
|
|
||||||
|
if t.LargeArc {
|
||||||
|
largeArcFlag = 1
|
||||||
|
}
|
||||||
|
if t.Clockwise {
|
||||||
|
sweepFlag = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%v,%v,%v,%d,%d,%v,%v", t.RX, t.RY, t.Angle, largeArcFlag, sweepFlag, t.X, t.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTuples[S fmt.Stringer](vs []S) string {
|
||||||
|
ss := make([]string, len(vs))
|
||||||
|
for i, v := range vs {
|
||||||
|
ss[i] = fmt.Sprint(v)
|
||||||
|
}
|
||||||
|
return strings.Join(ss, " ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
DominantBaseline int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DominantBaselineAuto DominantBaseline = iota
|
||||||
|
DominantBaselineTextBottom
|
||||||
|
DominantBaselineAlphabetic
|
||||||
|
DominantBaselineIdeographic
|
||||||
|
DominantBaselineMiddle
|
||||||
|
DominantBaselineCentral
|
||||||
|
DominantBaselineMathematical
|
||||||
|
DominantBaselineHanging
|
||||||
|
DominantBaselineTextTop
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a DominantBaseline) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf("%q", a), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a DominantBaseline) PrintKey() string {
|
||||||
|
return "dominant-baseline"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a DominantBaseline) String() string {
|
||||||
|
switch a {
|
||||||
|
case DominantBaselineTextBottom:
|
||||||
|
return "text-bottom"
|
||||||
|
case DominantBaselineAlphabetic:
|
||||||
|
return "alphabetic"
|
||||||
|
case DominantBaselineIdeographic:
|
||||||
|
return "ideographic"
|
||||||
|
case DominantBaselineMiddle:
|
||||||
|
return "middle"
|
||||||
|
case DominantBaselineCentral:
|
||||||
|
return "central"
|
||||||
|
case DominantBaselineMathematical:
|
||||||
|
return "mathematical"
|
||||||
|
case DominantBaselineHanging:
|
||||||
|
return "hanging"
|
||||||
|
case DominantBaselineTextTop:
|
||||||
|
return "text-top"
|
||||||
|
default:
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
DX struct {
|
||||||
|
*LengthAttr[DXTag]
|
||||||
|
}
|
||||||
|
DXP struct {
|
||||||
|
PercentageAttr[DXTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
DXTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsDX() DX {
|
||||||
|
return DX{LengthAttr: (*LengthAttr[DXTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsDX() DXP {
|
||||||
|
return DXP{PercentageAttr: (PercentageAttr[DXTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DXTag) PrintTag() string {
|
||||||
|
return "dx"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
DY struct {
|
||||||
|
*LengthAttr[DYTag]
|
||||||
|
}
|
||||||
|
DYP struct {
|
||||||
|
PercentageAttr[DYTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
DYTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsDY() DY {
|
||||||
|
return DY{LengthAttr: (*LengthAttr[DYTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsDY() DYP {
|
||||||
|
return DYP{PercentageAttr: (PercentageAttr[DYTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DYTag) PrintTag() string {
|
||||||
|
return "dy"
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Element is a convenience for implementing SVG element models.
|
||||||
|
// Element implements MarshalerReader.
|
||||||
|
Element[T Tag, A Attribute, C MarshalerReader] struct {
|
||||||
|
Attributes []A
|
||||||
|
Children []C
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *Element[T, A, C]) Attr(as ...A) *Element[T, A, C] {
|
||||||
|
e.Attributes = append(e.Attributes, as...)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Element[T, A, C]) Child(cs ...C) *Element[T, A, C] {
|
||||||
|
e.Children = append(e.Children, cs...)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Element[T, A, C]) GetMarkup() string {
|
||||||
|
attrs := make([]string, len(e.Attributes))
|
||||||
|
for i, a := range e.Attributes {
|
||||||
|
attrs[i] = PrintAttribute(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
children := make([]string, len(e.Children))
|
||||||
|
for i, c := range e.Children {
|
||||||
|
children[i] = c.GetMarkup()
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := e.PrintTag()
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`<%s %s>%s</%s>`,
|
||||||
|
tag,
|
||||||
|
strings.Join(attrs, " "),
|
||||||
|
strings.Join(children, ""),
|
||||||
|
tag,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Element[T, A, C]) GetMarkupReader() io.Reader {
|
||||||
|
return newElementReader(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Element[T, _, _]) PrintTag() string {
|
||||||
|
var t T
|
||||||
|
return t.PrintTag()
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
elementReader[T Tag, A Attribute, C MarshalerReader] struct {
|
||||||
|
element Element[T, A, C]
|
||||||
|
buf *bytes.Buffer
|
||||||
|
|
||||||
|
openTagRead bool
|
||||||
|
numChildrenRead int
|
||||||
|
childrenRead bool
|
||||||
|
childReader io.Reader
|
||||||
|
closeTagRead bool
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newElementReader[T Tag, A Attribute, C MarshalerReader](e Element[T, A, C]) *elementReader[T, A, C] {
|
||||||
|
return &elementReader[T, A, C]{
|
||||||
|
element: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *elementReader[T, A, C]) Read(p []byte) (totalRead int, err error) {
|
||||||
|
if e.buf == nil {
|
||||||
|
// just started reading.
|
||||||
|
// buffer the open tag
|
||||||
|
|
||||||
|
e.buf = new(bytes.Buffer)
|
||||||
|
|
||||||
|
// bytes.Buffer never returns an error on Write()
|
||||||
|
e.buf.WriteByte('<')
|
||||||
|
e.buf.WriteString(e.element.PrintTag())
|
||||||
|
for _, a := range e.element.Attributes {
|
||||||
|
e.buf.WriteByte(' ')
|
||||||
|
e.buf.WriteString(PrintAttribute(a))
|
||||||
|
}
|
||||||
|
e.buf.WriteByte('>')
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.openTagRead {
|
||||||
|
// read the open tag
|
||||||
|
|
||||||
|
n, err := e.buf.Read(p)
|
||||||
|
totalRead += n
|
||||||
|
if (err != nil && !errors.Is(err, io.EOF)) || n >= len(p) {
|
||||||
|
return totalRead, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// done reading the open tag
|
||||||
|
|
||||||
|
e.openTagRead = true
|
||||||
|
e.buf.Reset()
|
||||||
|
|
||||||
|
p = p[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the children
|
||||||
|
|
||||||
|
for ; e.numChildrenRead < len(e.element.Children); e.numChildrenRead += 1 {
|
||||||
|
if e.childReader == nil {
|
||||||
|
e.childReader = e.element.Children[e.numChildrenRead].GetMarkupReader()
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the child
|
||||||
|
|
||||||
|
n, err := e.childReader.Read(p)
|
||||||
|
totalRead += n
|
||||||
|
if (err != nil && !errors.Is(err, io.EOF)) || n >= len(p) {
|
||||||
|
return totalRead, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// done reading the child
|
||||||
|
|
||||||
|
e.childReader = nil
|
||||||
|
|
||||||
|
p = p[n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.childrenRead {
|
||||||
|
// done reading the children
|
||||||
|
|
||||||
|
e.childrenRead = true
|
||||||
|
|
||||||
|
// buffer the close tag
|
||||||
|
|
||||||
|
e.buf.Write([]byte("</"))
|
||||||
|
e.buf.WriteString(e.element.PrintTag())
|
||||||
|
e.buf.WriteByte('>')
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.closeTagRead {
|
||||||
|
n, err := e.buf.Read(p)
|
||||||
|
totalRead += n
|
||||||
|
if (err != nil && !errors.Is(err, io.EOF)) || n >= len(p) {
|
||||||
|
return totalRead, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// done writing the close tag
|
||||||
|
|
||||||
|
e.closeTagRead = true
|
||||||
|
return totalRead, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Ellipse = VoidElement[EllipseTag, EllipseAttribute]
|
||||||
|
|
||||||
|
EllipseTag struct{}
|
||||||
|
|
||||||
|
EllipseAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsEllipseAttribute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (EllipseTag) PrintTag() string {
|
||||||
|
return "ellipse"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RX) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RXP) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RY) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RYP) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CX) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CXP) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CY) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CYP) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Stroke) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StrokeWidth) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Fill) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Style) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VectorEffect) IsEllipseAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Fill string
|
||||||
|
)
|
||||||
|
|
||||||
|
func (Fill) PrintKey() string {
|
||||||
|
return "fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Fill) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf(`%q`, string(f)), true
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
FontSize struct {
|
||||||
|
*LengthAttr[FontSizeTag]
|
||||||
|
}
|
||||||
|
FontSizeP struct {
|
||||||
|
PercentageAttr[FontSizeTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
FontSizeTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsFontSize() FontSize {
|
||||||
|
return FontSize{LengthAttr: (*LengthAttr[FontSizeTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsFontSize() FontSizeP {
|
||||||
|
return FontSizeP{PercentageAttr: (PercentageAttr[FontSizeTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (FontSizeTag) PrintTag() string {
|
||||||
|
return "font-size"
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
G struct {
|
||||||
|
Element[GTag, GAttribute, GChildren]
|
||||||
|
}
|
||||||
|
GTag struct{}
|
||||||
|
|
||||||
|
GAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsGAttribute()
|
||||||
|
}
|
||||||
|
|
||||||
|
GChildren = MarshalerReader
|
||||||
|
)
|
||||||
|
|
||||||
|
func (GTag) PrintTag() string {
|
||||||
|
return "g"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Transform) IsGAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (X) IsGAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (XP) IsGAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Y) IsGAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YP) IsGAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Class) IsGAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Height struct {
|
||||||
|
*LengthAttr[HeightTag]
|
||||||
|
}
|
||||||
|
HeightP struct {
|
||||||
|
PercentageAttr[HeightTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
HeightTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsHeight() Height {
|
||||||
|
return Height{LengthAttr: (*LengthAttr[HeightTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsHeightP() HeightP {
|
||||||
|
return HeightP{PercentageAttr: (PercentageAttr[HeightTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (HeightTag) PrintTag() string {
|
||||||
|
return "height"
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Length struct {
|
||||||
|
number float64
|
||||||
|
absUnit AbsoluteLengthUnit
|
||||||
|
relUnit RelativeLengthUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
LengthAttr[T Tag] Length
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLength(n float64) *Length {
|
||||||
|
return &Length{
|
||||||
|
number: n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Length) Number(n float64) *Length {
|
||||||
|
l.number = n
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Length) Unit(u AbsoluteLengthUnit) *Length {
|
||||||
|
l.absUnit = u
|
||||||
|
l.relUnit = 0
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Length) RUnit(u RelativeLengthUnit) *Length {
|
||||||
|
l.relUnit = u
|
||||||
|
l.absUnit = 0
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Length) String() string {
|
||||||
|
if l.absUnit != 0 {
|
||||||
|
return fmt.Sprintf("%v%s", l.number, l.absUnit)
|
||||||
|
}
|
||||||
|
if l.relUnit != 0 {
|
||||||
|
return fmt.Sprintf("%v%s", l.number, l.relUnit)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", l.number)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LengthAttr[T]) PrintKey() string {
|
||||||
|
var tag T
|
||||||
|
return tag.PrintTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a LengthAttr[T]) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf(`"%s"`, Length(a)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relative length units
|
||||||
|
type RelativeLengthUnit int
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ RelativeLengthUnit = iota
|
||||||
|
|
||||||
|
// based on font
|
||||||
|
Cap
|
||||||
|
Ch
|
||||||
|
Em
|
||||||
|
Ex
|
||||||
|
Ic
|
||||||
|
Lh
|
||||||
|
|
||||||
|
// based on root element's font
|
||||||
|
Rcap
|
||||||
|
Rch
|
||||||
|
Rem
|
||||||
|
Rex
|
||||||
|
Ric
|
||||||
|
Rlh
|
||||||
|
|
||||||
|
// based on viewport
|
||||||
|
Vh
|
||||||
|
Vw
|
||||||
|
Vmax
|
||||||
|
Vmin
|
||||||
|
Vb
|
||||||
|
Vi
|
||||||
|
// small viewport
|
||||||
|
Svh
|
||||||
|
Svw
|
||||||
|
Svmax
|
||||||
|
Svmin
|
||||||
|
Svb
|
||||||
|
Svi
|
||||||
|
// large viewport
|
||||||
|
Lvh
|
||||||
|
Lvw
|
||||||
|
Lvmax
|
||||||
|
Lvmin
|
||||||
|
Lvb
|
||||||
|
Lvi
|
||||||
|
// dynamic viewport
|
||||||
|
Dvh
|
||||||
|
Dvw
|
||||||
|
Dvmax
|
||||||
|
Dvmin
|
||||||
|
Dvb
|
||||||
|
Dvi
|
||||||
|
|
||||||
|
// container query
|
||||||
|
Cqw
|
||||||
|
Cqh
|
||||||
|
Cqi
|
||||||
|
Cqb
|
||||||
|
Cqmin
|
||||||
|
Cqmax
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u RelativeLengthUnit) String() string {
|
||||||
|
switch u {
|
||||||
|
case Cap:
|
||||||
|
return "cap"
|
||||||
|
case Ch:
|
||||||
|
return "ch"
|
||||||
|
case Em:
|
||||||
|
return "em"
|
||||||
|
case Ex:
|
||||||
|
return "ex"
|
||||||
|
case Ic:
|
||||||
|
return "ic"
|
||||||
|
case Lh:
|
||||||
|
return "lh"
|
||||||
|
case Rcap:
|
||||||
|
return "rcap"
|
||||||
|
case Rch:
|
||||||
|
return "rch"
|
||||||
|
case Rem:
|
||||||
|
return "rem"
|
||||||
|
case Rex:
|
||||||
|
return "rex"
|
||||||
|
case Ric:
|
||||||
|
return "ric"
|
||||||
|
case Rlh:
|
||||||
|
return "rlh"
|
||||||
|
case Vh:
|
||||||
|
return "vh"
|
||||||
|
case Vw:
|
||||||
|
return "vw"
|
||||||
|
case Vmax:
|
||||||
|
return "vmax"
|
||||||
|
case Vmin:
|
||||||
|
return "vmin"
|
||||||
|
case Vb:
|
||||||
|
return "vb"
|
||||||
|
case Vi:
|
||||||
|
return "vi"
|
||||||
|
case Svh:
|
||||||
|
return "svh"
|
||||||
|
case Svw:
|
||||||
|
return "svw"
|
||||||
|
case Svmax:
|
||||||
|
return "svmax"
|
||||||
|
case Svmin:
|
||||||
|
return "svmin"
|
||||||
|
case Svb:
|
||||||
|
return "svb"
|
||||||
|
case Svi:
|
||||||
|
return "svi"
|
||||||
|
case Lvh:
|
||||||
|
return "lvh"
|
||||||
|
case Lvw:
|
||||||
|
return "lvw"
|
||||||
|
case Lvmax:
|
||||||
|
return "lvmax"
|
||||||
|
case Lvmin:
|
||||||
|
return "lvmin"
|
||||||
|
case Lvb:
|
||||||
|
return "lvb"
|
||||||
|
case Lvi:
|
||||||
|
return "lvi"
|
||||||
|
case Dvh:
|
||||||
|
return "dvh"
|
||||||
|
case Dvw:
|
||||||
|
return "dvw"
|
||||||
|
case Dvmax:
|
||||||
|
return "dvmax"
|
||||||
|
case Dvmin:
|
||||||
|
return "dvmin"
|
||||||
|
case Dvb:
|
||||||
|
return "dvb"
|
||||||
|
case Dvi:
|
||||||
|
return "dvi"
|
||||||
|
case Cqw:
|
||||||
|
return "cqw"
|
||||||
|
case Cqh:
|
||||||
|
return "cqh"
|
||||||
|
case Cqi:
|
||||||
|
return "cqi"
|
||||||
|
case Cqb:
|
||||||
|
return "cqb"
|
||||||
|
case Cqmin:
|
||||||
|
return "cqmin"
|
||||||
|
case Cqmax:
|
||||||
|
return "cqmax"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute length units
|
||||||
|
type AbsoluteLengthUnit int
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ AbsoluteLengthUnit = iota
|
||||||
|
Px
|
||||||
|
Cm
|
||||||
|
Mm
|
||||||
|
Q
|
||||||
|
In
|
||||||
|
Pc
|
||||||
|
Pt
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u AbsoluteLengthUnit) String() string {
|
||||||
|
switch u {
|
||||||
|
case Px:
|
||||||
|
return "px"
|
||||||
|
case Cm:
|
||||||
|
return "cm"
|
||||||
|
case Mm:
|
||||||
|
return "mm"
|
||||||
|
case Q:
|
||||||
|
return "q"
|
||||||
|
case In:
|
||||||
|
return "in"
|
||||||
|
case Pc:
|
||||||
|
return "pc"
|
||||||
|
case Pt:
|
||||||
|
return "pt"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
LengthAdjust int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
LengthAdjustSpacing LengthAdjust = iota
|
||||||
|
LengthAdjustSpacingAndGlyph
|
||||||
|
)
|
||||||
|
|
||||||
|
func (LengthAdjust) PrintTag() string {
|
||||||
|
return "lengthAdjust"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LengthAdjust) PrintValue() (string, bool) {
|
||||||
|
switch l {
|
||||||
|
case LengthAdjustSpacingAndGlyph:
|
||||||
|
return "spacingAndGlyphs", true
|
||||||
|
default:
|
||||||
|
return "spacing", true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Line = VoidElement[LineTag, LineAttribute]
|
||||||
|
|
||||||
|
LineTag struct{}
|
||||||
|
|
||||||
|
LineAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsLineAttribute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (LineTag) PrintTag() string {
|
||||||
|
return "line"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (X1) IsLineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (X2) IsLineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Y1) IsLineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Y2) IsLineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsLineAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type (
|
||||||
|
// All SVG element models must satisfy the Marshaler interface.
|
||||||
|
Marshaler interface {
|
||||||
|
GetMarkup() string
|
||||||
|
}
|
||||||
|
|
||||||
|
Reader interface {
|
||||||
|
GetMarkupReader() io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
MarshalerReader interface {
|
||||||
|
Marshaler
|
||||||
|
Reader
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Path = VoidElement[PathTag, PathAttribute]
|
||||||
|
|
||||||
|
PathTag struct{}
|
||||||
|
|
||||||
|
PathAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsPathAttribute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (PathTag) PrintTag() string {
|
||||||
|
return "path"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (D) IsPathAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsPathAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
PathLength float64
|
||||||
|
)
|
||||||
|
|
||||||
|
func (PathLength) PrintKey() string {
|
||||||
|
return "pathLength"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l PathLength) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf(`"%v"`, l), true
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Percentage float64
|
||||||
|
|
||||||
|
PercentageAttr[T Tag] Percentage
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Percentage) String() string {
|
||||||
|
return fmt.Sprintf("%v%%", float64(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PercentageAttr[T]) PrintKey() string {
|
||||||
|
var t T
|
||||||
|
return t.PrintTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a PercentageAttr[T]) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf("%q", Percentage(a)), true
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Points []Point
|
||||||
|
Point struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (Points) PrintKey() string {
|
||||||
|
return "points"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps Points) PrintValue() (string, bool) {
|
||||||
|
ss := make([]string, len(ps))
|
||||||
|
for i, p := range ps {
|
||||||
|
ss[i] = fmt.Sprintf("%v,%v", p.X, p.Y)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%q", strings.Join(ss, " ")), true
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Polygon = VoidElement[PolygonTag, PolygonAttribute]
|
||||||
|
|
||||||
|
PolygonTag struct{}
|
||||||
|
|
||||||
|
PolygonAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsPolygonAttribute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (PolygonTag) PrintTag() string {
|
||||||
|
return "path"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Points) IsPolygonAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsPolygonAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
Polyline = VoidElement[PolylineTag, PolylineAttribute]
|
||||||
|
|
||||||
|
PolylineTag struct{}
|
||||||
|
|
||||||
|
PolylineAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsPolylineAttribute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (PolylineTag) PrintTag() string {
|
||||||
|
return "polyline"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Points) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Stroke) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Fill) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StrokeWidth) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StrokeWidthP) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VectorEffect) IsPolylineAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// TODO: consider reworking this WHOLE API/PACKAGE to make it more of a chaining API.
|
||||||
|
|
||||||
|
type (
|
||||||
|
PreserveAspectRatio struct {
|
||||||
|
Align *AlignValue
|
||||||
|
MeetOrSlice MeetOrSliceValue
|
||||||
|
}
|
||||||
|
|
||||||
|
AlignValue struct {
|
||||||
|
X AlignValueComponent
|
||||||
|
Y AlignValueComponent
|
||||||
|
}
|
||||||
|
AlignValueComponent int
|
||||||
|
MeetOrSliceValue int
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlignMid AlignValueComponent = iota
|
||||||
|
AlignMin
|
||||||
|
AlignMax
|
||||||
|
|
||||||
|
_ MeetOrSliceValue = iota
|
||||||
|
Meet
|
||||||
|
Slice
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r PreserveAspectRatio) PrintKey() string {
|
||||||
|
return "preserveAspectRatio"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r PreserveAspectRatio) PrintValue() (string, bool) {
|
||||||
|
if mos := r.MeetOrSlice.String(); mos != "" {
|
||||||
|
return fmt.Sprintf(`"%s %s"`, r.Align, mos), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%q", r.Align), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AlignValue) String() string {
|
||||||
|
if v == nil {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("x%sY%s", v.X, v.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v AlignValueComponent) String() string {
|
||||||
|
switch v {
|
||||||
|
case AlignMin:
|
||||||
|
return "Min"
|
||||||
|
case AlignMax:
|
||||||
|
return "Max"
|
||||||
|
default:
|
||||||
|
return "Mid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v MeetOrSliceValue) String() string {
|
||||||
|
switch v {
|
||||||
|
case Meet:
|
||||||
|
return "meet"
|
||||||
|
case Slice:
|
||||||
|
return "slice"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
R struct {
|
||||||
|
*LengthAttr[RTag]
|
||||||
|
}
|
||||||
|
RP struct {
|
||||||
|
PercentageAttr[RTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
RTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsR() R {
|
||||||
|
return R{LengthAttr: (*LengthAttr[RTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsR() RP {
|
||||||
|
return RP{PercentageAttr: (PercentageAttr[RTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RTag) PrintTag() string {
|
||||||
|
return "r"
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Rect models a <rect> element
|
||||||
|
Rect = VoidElement[RectTag, RectAttribute]
|
||||||
|
RectTag struct{}
|
||||||
|
|
||||||
|
// SVGAttributes are Attributes allowed on SVGs
|
||||||
|
RectAttribute interface {
|
||||||
|
Attribute
|
||||||
|
IsRectAttribute()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (RectTag) PrintTag() string {
|
||||||
|
return "rect"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (X) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (XP) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Y) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (YP) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Width) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (WidthP) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Height) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (HeightP) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RX) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RXP) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RY) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RYP) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (PathLength) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Class) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Fill) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Stroke) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (StrokeWidth) IsRectAttribute() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Style) IsRectAttribute() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type (
|
||||||
|
Rotate float64
|
||||||
|
RotateAuto struct{}
|
||||||
|
RotateAutoReverse struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r Rotate) PrintKey() string {
|
||||||
|
return "rotate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Rotate) PrintValue() (string, bool) {
|
||||||
|
return fmt.Sprintf(`"%v"`, float64(r)), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RotateAuto) PrintKey() string {
|
||||||
|
return "rotate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RotateAuto) PrintValue() (string, bool) {
|
||||||
|
return `"auto"`, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RotateAutoReverse) PrintKey() string {
|
||||||
|
return "rotate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RotateAutoReverse) PrintValue() (string, bool) {
|
||||||
|
return `"auto-reverse"`, true
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
RX struct {
|
||||||
|
*LengthAttr[RXTag]
|
||||||
|
}
|
||||||
|
RXP struct {
|
||||||
|
PercentageAttr[RXTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
RXTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsRX() RX {
|
||||||
|
return RX{LengthAttr: (*LengthAttr[RXTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsRX() RXP {
|
||||||
|
return RXP{PercentageAttr: (PercentageAttr[RXTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RXTag) PrintTag() string {
|
||||||
|
return "rx"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package svg
|
||||||
|
|
||||||
|
type (
|
||||||
|
RY struct {
|
||||||
|
*LengthAttr[RYTag]
|
||||||
|
}
|
||||||
|
RYP struct {
|
||||||
|
PercentageAttr[RYTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
RYTag struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (l *Length) AsRY() RY {
|
||||||
|
return RY{LengthAttr: (*LengthAttr[RYTag])(l)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Percentage) AsRY() RYP {
|
||||||
|
return RYP{PercentageAttr: (PercentageAttr[RYTag])(p)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RYTag) PrintTag() string {
|
||||||
|
return "ry"
|
||||||
|
}
|
||||||