diff --git a/api/resolvers/growth.js b/api/resolvers/growth.js
index 9a80e7f9..8dce1d72 100644
--- a/api/resolvers/growth.js
+++ b/api/resolvers/growth.js
@@ -52,9 +52,29 @@ export function intervalClause (when, table, and) {
return `"${table}".created_at >= now_utc() - interval '${interval(when)}' ${and ? 'AND' : ''} `
}
+export function viewIntervalClause (when, view, and) {
+ if (when === 'forever') {
+ return and ? '' : 'TRUE'
+ }
+
+ return `"${view}".day >= date_trunc('day', timezone('America/Chicago', now() - interval '${interval(when)}')) ${and ? 'AND' : ''} `
+}
+
export default {
Query: {
registrationGrowth: async (parent, { when }, { models }) => {
+ if (when !== 'day') {
+ return await models.$queryRaw(`
+ SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
+ json_build_object('name', 'referrals', 'value', sum(referrals)),
+ json_build_object('name', 'organic', 'value', sum(organic))
+ ) AS data
+ FROM reg_growth_days
+ WHERE ${viewIntervalClause(when, 'reg_growth_days', false)}
+ GROUP BY time
+ ORDER BY time ASC`)
+ }
+
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
@@ -67,6 +87,22 @@ export default {
ORDER BY time ASC`)
},
spenderGrowth: async (parent, { when }, { models }) => {
+ if (when !== 'day') {
+ return await models.$queryRaw(`
+ SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
+ json_build_object('name', 'any', 'value', sum("any")),
+ json_build_object('name', 'jobs', 'value', sum(jobs)),
+ json_build_object('name', 'boost', 'value', sum(boost)),
+ json_build_object('name', 'fees', 'value', sum(fees)),
+ json_build_object('name', 'tips', 'value', sum(tips)),
+ json_build_object('name', 'donation', 'value', sum(donations))
+ ) AS data
+ FROM spender_growth_days
+ WHERE ${viewIntervalClause(when, 'spender_growth_days', false)}
+ GROUP BY time
+ ORDER BY time ASC`)
+ }
+
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
@@ -90,6 +126,19 @@ export default {
ORDER BY time ASC`)
},
itemGrowth: async (parent, { when }, { models }) => {
+ if (when !== 'day') {
+ return await models.$queryRaw(`
+ SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
+ json_build_object('name', 'posts', 'value', sum(posts)),
+ json_build_object('name', 'comments', 'value', sum(comments)),
+ json_build_object('name', 'jobs', 'value', sum(jobs))
+ ) AS data
+ FROM item_growth_days
+ WHERE ${viewIntervalClause(when, 'item_growth_days', false)}
+ GROUP BY time
+ ORDER BY time ASC`)
+ }
+
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
@@ -103,6 +152,21 @@ export default {
ORDER BY time ASC`)
},
spendingGrowth: async (parent, { when }, { models }) => {
+ if (when !== 'day') {
+ return await models.$queryRaw(`
+ SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
+ json_build_object('name', 'jobs', 'value', sum(jobs)),
+ json_build_object('name', 'boost', 'value', sum(boost)),
+ json_build_object('name', 'fees', 'value', sum(fees)),
+ json_build_object('name', 'tips', 'value', sum(tips)),
+ json_build_object('name', 'donations', 'value', sum(donations))
+ ) AS data
+ FROM spending_growth_days
+ WHERE ${viewIntervalClause(when, 'spending_growth_days', false)}
+ GROUP BY time
+ ORDER BY time ASC`)
+ }
+
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
@@ -125,6 +189,21 @@ export default {
ORDER BY time ASC`)
},
stackerGrowth: async (parent, { when }, { models }) => {
+ if (when !== 'day') {
+ return await models.$queryRaw(`
+ SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
+ json_build_object('name', 'any', 'value', sum("any")),
+ json_build_object('name', 'posts', 'value', sum(posts)),
+ json_build_object('name', 'comments', 'value', sum(comments)),
+ json_build_object('name', 'rewards', 'value', sum(rewards)),
+ json_build_object('name', 'referrals', 'value', sum(referrals))
+ ) AS data
+ FROM stackers_growth_days
+ WHERE ${viewIntervalClause(when, 'stackers_growth_days', false)}
+ GROUP BY time
+ ORDER BY time ASC`)
+ }
+
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
@@ -152,6 +231,20 @@ export default {
ORDER BY time ASC`)
},
stackingGrowth: async (parent, { when }, { models }) => {
+ if (when !== 'day') {
+ return await models.$queryRaw(`
+ SELECT date_trunc('${timeUnit(when)}', day) as time, json_build_array(
+ json_build_object('name', 'rewards', 'value', sum(rewards)),
+ json_build_object('name', 'posts', 'value', sum(posts)),
+ json_build_object('name', 'comments', 'value', sum(comments)),
+ json_build_object('name', 'referrals', 'value', sum(referrals))
+ ) AS data
+ FROM stacking_growth_days
+ WHERE ${viewIntervalClause(when, 'stacking_growth_days', false)}
+ GROUP BY time
+ ORDER BY time ASC`)
+ }
+
return await models.$queryRaw(
`${withClause(when)}
SELECT time, json_build_array(
diff --git a/api/resolvers/index.js b/api/resolvers/index.js
index 44cc7e85..a8ba9572 100644
--- a/api/resolvers/index.js
+++ b/api/resolvers/index.js
@@ -7,7 +7,7 @@ import notifications from './notifications'
import invite from './invite'
import sub from './sub'
import upload from './upload'
-// import growth from './growth'
+import growth from './growth'
import search from './search'
import rewards from './rewards'
import referrals from './referrals'
@@ -15,4 +15,4 @@ import price from './price'
import { GraphQLJSONObject } from 'graphql-type-json'
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
- upload, search, rewards, referrals, price, { JSONObject: GraphQLJSONObject }]
+ upload, search, growth, rewards, referrals, price, { JSONObject: GraphQLJSONObject }]
diff --git a/api/typeDefs/growth.js b/api/typeDefs/growth.js
index ba70881b..2b41fa8f 100644
--- a/api/typeDefs/growth.js
+++ b/api/typeDefs/growth.js
@@ -1,20 +1,20 @@
import { gql } from 'apollo-server-micro'
-// extend type Query {
-// registrationGrowth(when: String): [TimeData!]!
-// itemGrowth(when: String): [TimeData!]!
-// spendingGrowth(when: String): [TimeData!]!
-// spenderGrowth(when: String): [TimeData!]!
-// stackingGrowth(when: String): [TimeData!]!
-// stackerGrowth(when: String): [TimeData!]!
-// }
-
export default gql`
type NameValue {
name: String!
value: Int!
}
+ extend type Query {
+ registrationGrowth(when: String): [TimeData!]!
+ itemGrowth(when: String): [TimeData!]!
+ spendingGrowth(when: String): [TimeData!]!
+ spenderGrowth(when: String): [TimeData!]!
+ stackingGrowth(when: String): [TimeData!]!
+ stackerGrowth(when: String): [TimeData!]!
+ }
+
type TimeData {
time: String!
data: [NameValue!]!
diff --git a/pages/users/[when].js b/pages/users/[when].js
new file mode 100644
index 00000000..6a3db864
--- /dev/null
+++ b/pages/users/[when].js
@@ -0,0 +1,94 @@
+import { gql } from '@apollo/client'
+import { getGetServerSideProps } from '../../api/ssrApollo'
+import Layout from '../../components/layout'
+import { Col, Row } from 'react-bootstrap'
+import { UsageHeader } from '../../components/usage-header'
+import { WhenLineChart, WhenAreaChart } from '../../components/when-charts'
+
+export const getServerSideProps = getGetServerSideProps(
+ gql`
+ query Growth($when: String!)
+ {
+ registrationGrowth(when: $when) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ itemGrowth(when: $when) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ spendingGrowth(when: $when) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ spenderGrowth(when: $when) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ stackingGrowth(when: $when) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ stackerGrowth(when: $when) {
+ time
+ data {
+ name
+ value
+ }
+ }
+ }`)
+
+export default function Growth ({
+ data: { registrationGrowth, itemGrowth, spendingGrowth, spenderGrowth, stackingGrowth, stackerGrowth }
+}) {
+ return (
+
+
+
+
+ stackers
+
+
+
+ stacking
+
+
+
+
+
+ spenders
+
+
+
+ spending
+
+
+
+
+
+ registrations
+
+
+
+ items
+
+
+
+
+ )
+}
diff --git a/pages/users/search.js b/pages/users/search.js
new file mode 100644
index 00000000..e5279ef5
--- /dev/null
+++ b/pages/users/search.js
@@ -0,0 +1,16 @@
+import Layout from '../../components/layout'
+import { getGetServerSideProps } from '../../api/ssrApollo'
+import { SeoSearch } from '../../components/seo'
+import { USER_SEARCH } from '../../fragments/users'
+import UserList from '../../components/user-list'
+
+export const getServerSideProps = getGetServerSideProps(USER_SEARCH, { limit: 21, similarity: 0.2 })
+
+export default function Index ({ data: { searchUsers } }) {
+ return (
+
+
+
+
+ )
+}
diff --git a/prisma/migrations/20230518191821_views/migration.sql b/prisma/migrations/20230518191821_views/migration.sql
new file mode 100644
index 00000000..d26e3276
--- /dev/null
+++ b/prisma/migrations/20230518191821_views/migration.sql
@@ -0,0 +1,112 @@
+-- the idea is that we refresh these every day after midnight texas time
+-- then anything but days doesn't need to be compute from scatch
+
+-- all days since the beginning of founding
+CREATE OR REPLACE VIEW days AS
+WITH range_values AS (
+ SELECT date_trunc('day', '2021-06-07'::timestamp) as minval,
+ date_trunc('day', timezone('America/Chicago', now() - interval '1 day')) as maxval)
+ SELECT generate_series(minval, maxval, interval '1 day') as day
+ FROM range_values;
+
+-- get registrations
+CREATE MATERIALIZED VIEW IF NOT EXISTS reg_growth_days AS
+SELECT day, count("referrerId") as referrals,
+ count(users.id) FILTER(WHERE id > 616) - count("inviteId") as organic
+FROM days
+LEFT JOIN users ON day = date_trunc('day', timezone('America/Chicago', created_at at time zone 'UTC'))
+GROUP BY day
+ORDER BY day ASC;
+
+-- get spenders
+CREATE MATERIALIZED VIEW IF NOT EXISTS spender_growth_days AS
+SELECT day, count(DISTINCT "userId") as any,
+ count(DISTINCT "userId") FILTER (WHERE act = 'STREAM') as jobs,
+ count(DISTINCT "userId") FILTER (WHERE act = 'BOOST') as boost,
+ count(DISTINCT "userId") FILTER (WHERE act = 'FEE') as fees,
+ count(DISTINCT "userId") FILTER (WHERE act = 'TIP') as tips,
+ count(DISTINCT "userId") FILTER (WHERE act = 'DONATION') as donations
+FROM days
+LEFT JOIN
+((SELECT "ItemAct".created_at, "userId", act::text as act
+ FROM "ItemAct")
+UNION ALL
+(SELECT created_at, "userId", 'DONATION' as act
+ FROM "Donation")) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
+GROUP BY day
+ORDER BY day ASC;
+
+-- get items
+CREATE MATERIALIZED VIEW IF NOT EXISTS item_growth_days AS
+SELECT day, count("parentId") as comments,
+ count("subName") FILTER (WHERE "subName" = 'jobs') as jobs,
+ count(id) FILTER (WHERE "parentId" IS NULL AND "subName" <> 'jobs') as posts
+FROM days
+LEFT JOIN "Item" ON day = date_trunc('day', timezone('America/Chicago', created_at at time zone 'UTC'))
+GROUP BY day
+ORDER BY day ASC;
+
+-- get spending
+CREATE MATERIALIZED VIEW IF NOT EXISTS spending_growth_days AS
+SELECT day, coalesce(floor(sum(msats) FILTER (WHERE act = 'STREAM')/1000), 0) as jobs,
+ coalesce(floor(sum(msats) FILTER (WHERE act = 'BOOST')/1000), 0) as boost,
+ coalesce(floor(sum(msats) FILTER (WHERE act NOT IN ('BOOST', 'TIP', 'STREAM', 'DONATION'))/1000), 0) as fees,
+ coalesce(floor(sum(msats) FILTER (WHERE act = 'TIP')/1000), 0) as tips,
+ coalesce(floor(sum(msats) FILTER (WHERE act = 'DONATION')/1000), 0) as donations
+FROM days
+LEFT JOIN
+((SELECT "ItemAct".created_at, msats, act::text as act
+ FROM "ItemAct")
+UNION ALL
+(SELECT created_at, sats * 1000 as msats, 'DONATION' as act
+ FROM "Donation")) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
+GROUP BY day
+ORDER BY day ASC;
+
+-- get stackers
+CREATE MATERIALIZED VIEW IF NOT EXISTS stackers_growth_days AS
+SELECT day, count(distinct user_id) as any,
+ count(distinct user_id) FILTER (WHERE type = 'POST') as posts,
+ count(distinct user_id) FILTER (WHERE type = 'COMMENT') as comments,
+ count(distinct user_id) FILTER (WHERE type = 'EARN') as rewards,
+ count(distinct user_id) FILTER (WHERE type = 'REFERRAL') as referrals
+FROM days
+LEFT JOIN
+((SELECT "ItemAct".created_at, "Item"."userId" as user_id, CASE WHEN "Item"."parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type
+ FROM "ItemAct"
+ JOIN "Item" on "ItemAct"."itemId" = "Item".id
+ WHERE "ItemAct".act = 'TIP')
+UNION ALL
+(SELECT created_at, "userId" as user_id, 'EARN' as type
+ FROM "Earn")
+UNION ALL
+ (SELECT created_at, "referrerId" as user_id, 'REFERRAL' as type
+ FROM "ReferralAct")) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
+GROUP BY day
+ORDER BY day ASC;
+
+-- get stacking
+CREATE MATERIALIZED VIEW IF NOT EXISTS stacking_growth_days AS
+SELECT day, coalesce(floor(sum(airdrop)/1000),0) as rewards,
+ coalesce(floor(sum(post)/1000),0) as posts,
+ coalesce(floor(sum(comment)/1000),0) as comments,
+ coalesce(floor(sum(referral)/1000),0) as referrals
+FROM days
+LEFT JOIN
+((SELECT "ItemAct".created_at, 0 as airdrop,
+ CASE WHEN "Item"."parentId" IS NULL THEN 0 ELSE "ItemAct".msats END as comment,
+ CASE WHEN "Item"."parentId" IS NULL THEN "ItemAct".msats ELSE 0 END as post,
+ 0 as referral
+ FROM "ItemAct"
+ JOIN "Item" on "ItemAct"."itemId" = "Item".id
+ WHERE "ItemAct".act = 'TIP')
+UNION ALL
+ (SELECT created_at, 0 as airdrop, 0 as post, 0 as comment, msats as referral
+ FROM "ReferralAct")
+UNION ALL
+(SELECT created_at, msats as airdrop, 0 as post, 0 as comment, 0 as referral
+ FROM "Earn")) u ON day = date_trunc('day', timezone('America/Chicago', u.created_at at time zone 'UTC'))
+GROUP BY day
+ORDER BY day ASC;
+
+
diff --git a/worker/index.js b/worker/index.js
index 57bc901f..96887071 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -15,6 +15,7 @@ const { nip57 } = require('./nostr')
const fetch = require('cross-fetch')
const { authenticatedLndGrpc } = require('ln-service')
+const { views } = require('./views')
async function work () {
const boss = new PgBoss(process.env.DATABASE_URL)
@@ -60,6 +61,7 @@ async function work () {
await boss.work('streak', computeStreaks(args))
await boss.work('checkStreak', checkStreak(args))
await boss.work('nip57', nip57(args))
+ await boss.work('views', views(args))
console.log('working jobs')
}
diff --git a/worker/views.js b/worker/views.js
new file mode 100644
index 00000000..9d4d0e8c
--- /dev/null
+++ b/worker/views.js
@@ -0,0 +1,14 @@
+function views ({ models }) {
+ return async function () {
+ console.log('refreshing views')
+
+ for (const view of ['reg_growth_days', 'spender_growth_days', 'item_growth_days',
+ 'spending_growth_days', 'stackers_growth_days', 'stacking_growth_days']) {
+ await models.$queryRaw(`REFRESH MATERIALIZED VIEW ${view}`)
+ }
+
+ console.log('done refreshing views')
+ }
+}
+
+module.exports = { views }