diff --git a/.vscode/settings.json b/.vscode/settings.json index e6c1e9928..9439716aa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "java.configuration.updateBuildConfiguration": "automatic", "cSpell.enabled": false, - "eslint.runtime": "node", // Deno configuration for supabase functions // NOTE - could be refactored to separate code-workspace // https://supabase.com/docs/guides/functions/local-development#setting-up-your-environment diff --git a/apps/picsa-apps/dashboard/src/app/data/navLinks.ts b/apps/picsa-apps/dashboard/src/app/data/navLinks.ts index c0feb141c..be5304250 100644 --- a/apps/picsa-apps/dashboard/src/app/data/navLinks.ts +++ b/apps/picsa-apps/dashboard/src/app/data/navLinks.ts @@ -46,6 +46,12 @@ export const DASHBOARD_NAV_LINKS: INavLink[] = [ label: 'Forecasts', href: '/forecast', }, + { + label: 'Admin', + href: '/admin', + // TODO - auth role + // TODO - import from module? + }, ], }, { diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/climate-api.mapping.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/climate-api.mapping.ts index db88bb339..7764328d5 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/climate-api.mapping.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/climate-api.mapping.ts @@ -5,20 +5,18 @@ import { IDeploymentRow } from '../deployment/types'; import { ClimateService } from './climate.service'; import type { ClimateApiService } from './climate-api.service'; import { - IClimateProductInsert, - IClimateProductRow, + IAPICountryCode, + IClimateSummaryRainfallInsert, + IClimateSummaryRainfallRow, IForecastInsert, IForecastRow, IStationInsert, IStationRow, } from './types'; -import type { components as ApiComponents } from './types/api'; export type IApiMapping = ReturnType; export type IApiMappingName = keyof IApiMapping; -export type IAPICountryCode = ApiComponents['schemas']['StationAndDefintionResponce']['country_code']; - // TODO - certain amount of boilerplate could be reduced // TODO - depends on climate api updates // TODO - most of these should be run on server as server functions @@ -38,7 +36,7 @@ export const ApiMapping = ( rainfallSummaries: async (station: IStationRow) => { const { country_code, station_id, station_name, id } = station; // TODO - add model type definitions for server rainfall summary response body - const { data, error } = await api + const { data: apiData, error } = await api .getObservableClient(`rainfallSummary_${id}`) .POST('/v1/annual_rainfall_summaries/', { body: { @@ -51,20 +49,22 @@ export const ApiMapping = ( }); if (error) throw error; // HACK - API issue returning huge data for some stations - if (data.data.length > 1000) { - console.error({ country_code, station_id, station_name, total_rows: data.data.length }); - throw new Error(`[rainfallSummary] Too many rows | ${station_name} ${data.data.length}`); + const { data, metadata } = apiData; + if (data.length > 1000) { + console.error({ country_code, station_id, station_name, total_rows: data.length }); + throw new Error(`[rainfallSummary] Too many rows | ${station_name} ${data.length}`); } // TODO - gen types and handle mapping - const entry: IClimateProductInsert = { - data: data as any, + const entry: IClimateSummaryRainfallInsert = { + data: data as any[], + metadata, station_id: id as string, - type: 'rainfallSummary', + country_code: country_code as any, }; const { data: dbData, error: dbError } = await db - .table('climate_products') - .upsert(entry) - .select<'*', IClimateProductRow>('*'); + .table('climate_summary_rainfall') + .upsert(entry) + .select<'*', IClimateSummaryRainfallRow>('*'); if (dbError) throw dbError; return dbData || []; }, diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/climate.module.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/climate.module.ts index 8430cb47f..65ea0fa64 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/climate.module.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/climate.module.ts @@ -16,6 +16,11 @@ import { StationDetailsPageComponent } from './pages/station-details/station-det redirectTo: 'station', pathMatch: 'full', }, + { + path: 'admin', + loadComponent: () => import('./pages/admin/admin.component').then((m) => m.ClimateAdminPageComponent), + // TODO - add auth route guards + }, { path: 'station', component: ClimateStationPageComponent, diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/climate.service.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/climate.service.ts index 4f77e6b95..55d2c66fd 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/climate.service.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/climate.service.ts @@ -9,9 +9,9 @@ import { map } from 'rxjs'; import { DeploymentDashboardService } from '../deployment/deployment.service'; import { IDeploymentRow } from '../deployment/types'; -import { ApiMapping, IAPICountryCode } from './climate-api.mapping'; +import { ApiMapping } from './climate-api.mapping'; import { ClimateApiService } from './climate-api.service'; -import { IStationRow } from './types'; +import { IAPICountryCode, IStationRow } from './types'; @Injectable({ providedIn: 'root' }) export class ClimateService extends PicsaAsyncService { diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.html b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.html new file mode 100644 index 000000000..185d92899 --- /dev/null +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.html @@ -0,0 +1,31 @@ +
+

Admin

+
+

See the table below for rainfall summary data for all stations

+ + + + +
+ + + + + + {{ value | date: 'mediumDate' }} + + +
diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.scss b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.spec.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.spec.ts new file mode 100644 index 000000000..6e49b1c25 --- /dev/null +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ClimateAdminPageComponent } from './admin.component'; + +describe('ClimateAdminPageComponent', () => { + let component: ClimateAdminPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ClimateAdminPageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ClimateAdminPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.ts new file mode 100644 index 000000000..bc6abdf02 --- /dev/null +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/admin/admin.component.ts @@ -0,0 +1,152 @@ +import { CommonModule, DatePipe } from '@angular/common'; +import { Component, computed, effect, signal } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { ActivatedRoute, Router } from '@angular/router'; +import { IDataTableOptions, PicsaDataTableComponent } from '@picsa/shared/features'; +import { SupabaseService } from '@picsa/shared/services/core/supabase'; +import { _wait, arrayToHashmap } from '@picsa/utils'; +import download from 'downloadjs'; +import JSZip from 'jszip'; +import { unparse } from 'papaparse'; + +import { DeploymentDashboardService } from '../../../deployment/deployment.service'; +import { ClimateService } from '../../climate.service'; +import type { IAnnualRainfallSummariesData, IClimateSummaryRainfallRow, IStationRow } from '../../types'; +import { hackConvertAPIDataToLegacyFormat } from '../station-details/components/rainfall-summary/rainfall-summary.utils'; + +interface IStationAdminSummary { + station_id: string; + row: IStationRow; + updated_at?: string; + rainfall_summary?: IClimateSummaryRainfallRow; + start_year?: number; + end_year?: number; +} + +const DISPLAY_COLUMNS: (keyof IStationAdminSummary)[] = ['station_id', 'updated_at', 'start_year', 'end_year']; + +/** + * TODOs - See #333 + */ + +@Component({ + selector: 'dashboard-climate-admin-page', + standalone: true, + imports: [CommonModule, DatePipe, MatButtonModule, MatIconModule, PicsaDataTableComponent], + templateUrl: './admin.component.html', + styleUrl: './admin.component.scss', +}) +export class ClimateAdminPageComponent { + public tableData = computed(() => { + const stations = this.service.stations(); + const rainfallSummaries = this.rainfallSummaries(); + return this.generateTableSummaryData(stations, rainfallSummaries); + }); + public tableOptions: IDataTableOptions = { + displayColumns: DISPLAY_COLUMNS, + handleRowClick: ({ station_id }: IStationAdminSummary) => + this.router.navigate(['../', 'station', station_id], { relativeTo: this.route }), + }; + public refreshCount = signal(-1); + + private rainfallSummaries = signal([]); + + constructor( + private service: ClimateService, + private deploymentService: DeploymentDashboardService, + private supabase: SupabaseService, + private router: Router, + private route: ActivatedRoute + ) { + effect( + () => { + const country_code = this.deploymentService.activeDeployment()?.country_code; + if (country_code) { + this.loadRainfallSummaries(country_code); + } + }, + { allowSignalWrites: true } + ); + } + private get db() { + return this.supabase.db.table('climate_summary_rainfall'); + } + + public async downloadAllStationsCSV() { + const zip = new JSZip(); + for (const summary of this.tableData()) { + const csvData = this.generateStationCSVDownload(summary); + if (csvData) { + zip.file(`${summary.row.station_id}.csv`, csvData); + } + } + const blob = await zip.generateAsync({ type: 'blob' }); + const country_code = this.deploymentService.activeDeployment()?.country_code; + download(blob, `${country_code}_rainfall_summaries.zip`); + } + + public downloadStationCSV(station: IStationAdminSummary) { + const csv = this.generateStationCSVDownload(station); + if (csv) { + download(csv, station.row.station_id, 'text/csv'); + } + } + + public async refreshAllStations() { + this.refreshCount.set(0); + const promises = this.tableData().map(async (station, i) => { + // hack - instead of queueing apply small offset between requests to reduce blocking + await _wait(200 * i); + await this.service.loadFromAPI.rainfallSummaries(station.row); + this.refreshCount.update((v) => v + 1); + }); + await Promise.all(promises); + await this.loadRainfallSummaries(this.deploymentService.activeDeployment()?.country_code as string); + } + + private generateStationCSVDownload(summary: IStationAdminSummary) { + const { rainfall_summary } = summary; + if (rainfall_summary && rainfall_summary.data) { + const data = rainfall_summary.data as any[]; + const csvData = hackConvertAPIDataToLegacyFormat(data); + const columns = Object.keys(csvData[0]); + const csv = unparse(csvData, { columns }); + return csv; + } + return undefined; + } + + private generateTableSummaryData(stations: IStationRow[], rainfallSummaries: IClimateSummaryRainfallRow[]) { + // NOTE - only single entry for rainfallSummary (not hashmapArray) + const rainfallSummariesHashmap = arrayToHashmap(rainfallSummaries, 'station_id'); + return stations.map((row) => { + const { station_id, id } = row; + const summary: IStationAdminSummary = { station_id, row }; + const rainfallSummary = rainfallSummariesHashmap[station_id]; + if (rainfallSummary) { + const { data, updated_at } = rainfallSummary; + summary.updated_at = updated_at; + summary.rainfall_summary = rainfallSummary; + const entries = data as IAnnualRainfallSummariesData[]; + summary.start_year = entries[0]?.year; + summary.end_year = entries[entries.length - 1]?.year; + } + return summary; + }); + } + + private async loadRainfallSummaries(country_code: string) { + const { data, error } = await this.db.select<'*', IClimateSummaryRainfallRow>('*').eq('country_code', country_code); + if (error) { + throw error; + } + this.rainfallSummaries.set( + data.map((el) => { + // HACK - to keep parent ref id includes full country prefix, remove for lookup + el.station_id = el.station_id.replace(`${country_code}/`, ''); + return el; + }) + ); + } +} diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.html b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.html index da805040c..40e033c0d 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.html +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.html @@ -8,7 +8,7 @@

Rainfall Summary

view_list Table - + diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.ts index e79881efd..db6b89b05 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.ts @@ -13,14 +13,17 @@ import { ChartConfiguration } from 'c3'; import { ClimateService } from '../../../../climate.service'; import { DashboardClimateApiStatusComponent, IApiStatusOptions } from '../../../../components/api-status/api-status'; -import { APITypes, IClimateProductRow, IStationRow } from '../../../../types'; - -type AnnualRainfallSummariesdata = APITypes.components['schemas']['AnnualRainfallSummariesdata']; +import { + IAnnualRainfallSummariesData, + IAnnualRainfallSummariesMetadata, + IClimateSummaryRainfallRow, + IStationRow, +} from '../../../../types'; +import { hackConvertAPIDataToLegacyFormat } from './rainfall-summary.utils'; interface IRainfallSummary { - // TODO - improve typings - data: any[]; - metadata: any; + data: IAnnualRainfallSummariesData[]; + metadata: IAnnualRainfallSummariesMetadata; } @Component({ @@ -41,7 +44,7 @@ interface IRainfallSummary { }) export class RainfallSummaryComponent { public summaryMetadata: IRainfallSummary['metadata'] = {}; - public summaryData: IRainfallSummary['data'] = []; + public summaryData: IStationData[] = []; public apiClientId: string; public chartDefintions: IChartMeta[] = []; public activeChartConfig: Partial; @@ -68,19 +71,18 @@ export class RainfallSummaryComponent { } private get db() { - return this.supabase.db.table('climate_products'); + return this.supabase.db.table('climate_summary_rainfall'); } private async loadActiveStation(station: IStationRow) { // Load data stored in supabase db if available. Otherwise load from api // TODO - nicer if could include db lookups as part of mapping doc const { data, error } = await this.db - .select<'*', IClimateProductRow>('*') + .select<'*', IClimateSummaryRainfallRow>('*') .eq('station_id', station.id) - .eq('type', 'rainfallSummary') .single(); if (data) { - this.loadData((data.data as any) || { data: [], metadata: {} }); + this.loadData(data); this.cdr.markForCheck(); } else { await this.refreshData(); @@ -94,7 +96,7 @@ export class RainfallSummaryComponent { const data = await this.service.loadFromAPI.rainfallSummaries(this.activeStation); const summary = data?.[0]; if (summary) { - this.loadData(summary.data as any); + this.loadData(summary); } } } @@ -106,33 +108,15 @@ export class RainfallSummaryComponent { } } - private loadData(summary: IRainfallSummary) { + private loadData(summary: IClimateSummaryRainfallRow) { console.log('load data', summary); this.tableOptions.exportFilename = `${this.activeStation.id}.csv`; const { data, metadata } = summary; - this.summaryData = this.convertAPIDataToLegacyFormat(data); + this.summaryData = hackConvertAPIDataToLegacyFormat(data); // this.summaryData = data; this.summaryMetadata = metadata; const { country_code } = this.activeStation; const definitions = CLIMATE_CHART_DEFINTIONS[country_code] || CLIMATE_CHART_DEFINTIONS.default; this.chartDefintions = Object.values(definitions); } - - // TODO - refactor components to use modern format - private convertAPIDataToLegacyFormat(apiData: AnnualRainfallSummariesdata[] = []) { - const data: Partial[] = apiData.map((el) => ({ - Year: el.year, - // HACK - use either end_rains or end_season depending on which has data populated - // TODO - push for single value to be populated at api level - End: el.end_rains_doy || el.end_season_doy, - // HACK - extreme events not currently supported - // Extreme_events: null as any, - Length: el.season_length, - // HACK - replace 0mm with null value - // HACK - newer api returning seasonal_rain instead of annual_rain - Rainfall: el.seasonal_rain || undefined, - Start: el.start_rains_doy, - })); - return data; - } } diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.utils.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.utils.ts new file mode 100644 index 000000000..bd429fb97 --- /dev/null +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/pages/station-details/components/rainfall-summary/rainfall-summary.utils.ts @@ -0,0 +1,33 @@ +import { IStationData } from '@picsa/models/src'; + +import { IAnnualRainfallSummariesData } from '../../../../types'; + +// TODO - refactor components to use modern format +export function hackConvertAPIDataToLegacyFormat(apiData: IAnnualRainfallSummariesData[] = []) { + const data: IStationData[] = apiData.map((el) => { + const entry: IStationData = { + Year: undefined as any, + Rainfall: undefined as any, + Start: undefined as any, + Length: undefined as any, + End: undefined as any, + Extreme_events: undefined as any, + }; + const { year, end_rains_doy, end_season_doy, season_length, seasonal_rain, annual_rain, start_rains_doy } = el; + if (typeof year === 'number') entry.Year = year; + // HACK - use either end_rains or end_season depending on which has data populated + // TODO - push for single value to be populated at api level + + if (typeof end_rains_doy === 'number') entry.End = end_rains_doy; + if (typeof end_season_doy === 'number') entry.End = end_season_doy; + if (typeof start_rains_doy === 'number') entry.Start = start_rains_doy; + if (typeof season_length === 'number') entry.Length = season_length; + // HACK - replace 0mm with null value + if (seasonal_rain) entry.Rainfall = seasonal_rain; + // HACK - mw uses seasonal_rain but zm uses annual_rainfall - API should return consistent + if (annual_rain) entry.Rainfall = annual_rain; + + return entry; + }); + return data; +} diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/types/db.d.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/types/db.d.ts index 32c0e1db3..d89d0aea1 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/types/db.d.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/types/db.d.ts @@ -1,8 +1,14 @@ // eslint-disable-next-line @nx/enforce-module-boundaries import type { Database } from '@picsa/server-types'; -export type IClimateProductRow = Database['public']['Tables']['climate_products']['Row']; -export type IClimateProductInsert = Database['public']['Tables']['climate_products']['Insert']; +import type { components as API } from './api'; + +// DB types (with some merged api) +export type IClimateSummaryRainfallRow = Database['public']['Tables']['climate_summary_rainfall']['Row'] & { + data: API['schemas']['AnnualRainfallSummariesdata'][]; + metadata: API['schemas']['AnnualRainfallSummariesMetadata']; +}; +export type IClimateSummaryRainfallInsert = Database['public']['Tables']['climate_summary_rainfall']['Insert']; export type IForecastRow = Database['public']['Tables']['climate_forecasts']['Row']; export type IForecastInsert = Database['public']['Tables']['climate_forecasts']['Insert']; diff --git a/apps/picsa-apps/dashboard/src/app/modules/climate/types/index.ts b/apps/picsa-apps/dashboard/src/app/modules/climate/types/index.ts index c5794de5c..11efe8f1d 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/climate/types/index.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/climate/types/index.ts @@ -1,4 +1,9 @@ import * as APITypes from './api'; export * from './db'; -export { APITypes }; +// Re-export specific API Types +type schemas = APITypes.components['schemas']; +export type IAnnualRainfallSummariesData = schemas['AnnualRainfallSummariesdata']; +export type IAnnualRainfallSummariesMetadata = schemas['AnnualRainfallSummariesMetadata']; + +export type IAPICountryCode = schemas['StationDataResponce']['country_code']; diff --git a/apps/picsa-apps/dashboard/src/app/modules/resources/pages/files/edit/resource-file-edit.component.ts b/apps/picsa-apps/dashboard/src/app/modules/resources/pages/files/edit/resource-file-edit.component.ts index 95ac79c99..220e79f52 100644 --- a/apps/picsa-apps/dashboard/src/app/modules/resources/pages/files/edit/resource-file-edit.component.ts +++ b/apps/picsa-apps/dashboard/src/app/modules/resources/pages/files/edit/resource-file-edit.component.ts @@ -84,6 +84,7 @@ export class ResourceFileEditComponent implements OnInit { modified_at: '', sort_order: 1, ...formValues, + country_code: formValues.country_code as any, }; return value; } diff --git a/apps/picsa-server/supabase/data/deployments_rows.csv b/apps/picsa-server/supabase/data/deployments_rows.csv index 02bf38b00..79520af51 100644 --- a/apps/picsa-server/supabase/data/deployments_rows.csv +++ b/apps/picsa-server/supabase/data/deployments_rows.csv @@ -2,9 +2,9 @@ id,country_code,label,configuration,variant,access_key_md5,public,icon_path demo_dev,demo,Extension,{},extension,,false,global/images/tutorial.svg global_extension,global,Extension,{},extension,,false,global/images/flags/global.svg global_farmer,global,Farmer,{},farmer,,false,global/images/flags/global.svg -mw_extension,mw,Extension,{},extension,,false,global/images/flags/mw.svg mw_farmer,mw,Farmer,{},farmer,,false,global/images/flags/mw.svg -mw_workshop,mw,Malawi Workshop,"{""climate_country_code"":""mw_workshops""}",extension,,true,global/images/flags/mw.svg -zm_extension,zm,Extension,{},extension,,false,global/images/flags/zm.svg zm_farmer,zm,Farmer,{},farmer,,false,global/images/flags/zm.svg -zm_workshop,zm,Zambia Workshop,"{""climate_country_code"":""zm_workshops""}",extension,,true,global/images/flags/zm.svg \ No newline at end of file +mw_extension,mw,Malawi,{},extension,,true,global/images/flags/mw.svg +mw_workshop,mw,Malawi Workshop,"{""climate_country_code"":""mw_workshops""}",extension,,false,global/images/flags/mw.svg +zm_extension,zm,Zambia,{},extension,,true,global/images/flags/zm.svg +zm_workshop,zm,Zambia Workshop,"{""climate_country_code"":""zm_workshops""}",extension,,false,global/images/flags/zm.svg \ No newline at end of file diff --git a/apps/picsa-server/supabase/migrations/20241014110300_climate_summary_rainfall.sql b/apps/picsa-server/supabase/migrations/20241014110300_climate_summary_rainfall.sql new file mode 100644 index 000000000..2537a4f6e --- /dev/null +++ b/apps/picsa-server/supabase/migrations/20241014110300_climate_summary_rainfall.sql @@ -0,0 +1,28 @@ +-- Replace generic `climate_products` table with more specific summaries + +-- BREAKING - will require re-seeding data +drop table if exists public.climate_products; + + +create table + public.climate_summary_rainfall ( + created_at timestamp with time zone not null default now(), + updated_at timestamp with time zone not null default now(), + station_id text not null, + country_code country_code not null, + metadata jsonb not null, + data jsonb[] not null, + constraint climate_summary_rainfall_pkey primary key ( + country_code, + station_id + ), + constraint climate_summary_rainfall_station_id_fkey foreign key (station_id) references climate_stations (id) on delete cascade + ) tablespace pg_default; + +-- Enable moddatetime extension to automatically populated `updated_at` columns +-- https://dev.to/paullaros/updating-timestamps-automatically-in-supabase-5f5o +CREATE EXTENSION IF NOT EXISTS "moddatetime" SCHEMA extensions; + +-- NOTE - required extensions - allow moddatetime +create trigger handle_updated_at before update on public.climate_summary_rainfall + for each row execute procedure moddatetime (updated_at); \ No newline at end of file diff --git a/apps/picsa-server/supabase/types/index.ts b/apps/picsa-server/supabase/types/index.ts index aa6ad2a89..42152ade3 100644 --- a/apps/picsa-server/supabase/types/index.ts +++ b/apps/picsa-server/supabase/types/index.ts @@ -82,35 +82,6 @@ export type Database = { }, ] } - climate_products: { - Row: { - created_at: string - data: Json - station_id: string - type: string - } - Insert: { - created_at?: string - data: Json - station_id: string - type: string - } - Update: { - created_at?: string - data?: Json - station_id?: string - type?: string - } - Relationships: [ - { - foreignKeyName: "climate_products_station_id_fkey" - columns: ["station_id"] - isOneToOne: false - referencedRelation: "climate_stations" - referencedColumns: ["id"] - }, - ] - } climate_stations: { Row: { country_code: string @@ -144,6 +115,41 @@ export type Database = { } Relationships: [] } + climate_summary_rainfall: { + Row: { + country_code: Database["public"]["Enums"]["country_code"] + created_at: string + data: Json[] + metadata: Json + station_id: string + updated_at: string + } + Insert: { + country_code: Database["public"]["Enums"]["country_code"] + created_at?: string + data: Json[] + metadata: Json + station_id: string + updated_at?: string + } + Update: { + country_code?: Database["public"]["Enums"]["country_code"] + created_at?: string + data?: Json[] + metadata?: Json + station_id?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "climate_summary_rainfall_station_id_fkey" + columns: ["station_id"] + isOneToOne: false + referencedRelation: "climate_stations" + referencedColumns: ["id"] + }, + ] + } crop_data: { Row: { created_at: string @@ -438,7 +444,7 @@ export type Database = { } resource_files: { Row: { - country_code: string | null + country_code: Database["public"]["Enums"]["country_code"] cover_image: string | null created_at: string description: string | null @@ -455,7 +461,7 @@ export type Database = { title: string | null } Insert: { - country_code?: string | null + country_code?: Database["public"]["Enums"]["country_code"] cover_image?: string | null created_at?: string description?: string | null @@ -472,7 +478,7 @@ export type Database = { title?: string | null } Update: { - country_code?: string | null + country_code?: Database["public"]["Enums"]["country_code"] cover_image?: string | null created_at?: string description?: string | null @@ -507,7 +513,7 @@ export type Database = { } resource_files_child: { Row: { - country_code: string | null + country_code: Database["public"]["Enums"]["country_code"] cover_image: string | null created_at: string description: string | null @@ -525,7 +531,7 @@ export type Database = { title: string | null } Insert: { - country_code?: string | null + country_code?: Database["public"]["Enums"]["country_code"] cover_image?: string | null created_at?: string description?: string | null @@ -543,7 +549,7 @@ export type Database = { title?: string | null } Update: { - country_code?: string | null + country_code?: Database["public"]["Enums"]["country_code"] cover_image?: string | null created_at?: string description?: string | null @@ -897,6 +903,101 @@ export type Database = { }, ] } + s3_multipart_uploads: { + Row: { + bucket_id: string + created_at: string + id: string + in_progress_size: number + key: string + owner_id: string | null + upload_signature: string + version: string + } + Insert: { + bucket_id: string + created_at?: string + id: string + in_progress_size?: number + key: string + owner_id?: string | null + upload_signature: string + version: string + } + Update: { + bucket_id?: string + created_at?: string + id?: string + in_progress_size?: number + key?: string + owner_id?: string | null + upload_signature?: string + version?: string + } + Relationships: [ + { + foreignKeyName: "s3_multipart_uploads_bucket_id_fkey" + columns: ["bucket_id"] + isOneToOne: false + referencedRelation: "buckets" + referencedColumns: ["id"] + }, + ] + } + s3_multipart_uploads_parts: { + Row: { + bucket_id: string + created_at: string + etag: string + id: string + key: string + owner_id: string | null + part_number: number + size: number + upload_id: string + version: string + } + Insert: { + bucket_id: string + created_at?: string + etag: string + id?: string + key: string + owner_id?: string | null + part_number: number + size?: number + upload_id: string + version: string + } + Update: { + bucket_id?: string + created_at?: string + etag?: string + id?: string + key?: string + owner_id?: string | null + part_number?: number + size?: number + upload_id?: string + version?: string + } + Relationships: [ + { + foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey" + columns: ["bucket_id"] + isOneToOne: false + referencedRelation: "buckets" + referencedColumns: ["id"] + }, + { + foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey" + columns: ["upload_id"] + isOneToOne: false + referencedRelation: "s3_multipart_uploads" + referencedColumns: ["id"] + }, + ] + } } Views: { [_ in never]: never @@ -936,6 +1037,41 @@ export type Database = { bucket_id: string }[] } + list_multipart_uploads_with_delimiter: { + Args: { + bucket_id: string + prefix_param: string + delimiter_param: string + max_keys?: number + next_key_token?: string + next_upload_token?: string + } + Returns: { + key: string + id: string + created_at: string + }[] + } + list_objects_with_delimiter: { + Args: { + bucket_id: string + prefix_param: string + delimiter_param: string + max_keys?: number + start_after?: string + next_token?: string + } + Returns: { + name: string + id: string + metadata: Json + updated_at: string + }[] + } + operation: { + Args: Record + Returns: string + } search: { Args: { prefix: string diff --git a/package.json b/package.json index 1bf78aa78..f9dcdef1c 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "execa": "^5.1.1", "fs-extra": "^10.1.0", - "husky": "^8.0.3", + "husky": "^9.1.6", "jest": "29.4.3", "jest-circus": "^29.5.0", "jest-environment-jsdom": "29.4.3", diff --git a/yarn.lock b/yarn.lock index 519b5d18a..e4703d330 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15538,12 +15538,12 @@ __metadata: languageName: node linkType: hard -"husky@npm:^8.0.3": - version: 8.0.3 - resolution: "husky@npm:8.0.3" +"husky@npm:^9.1.6": + version: 9.1.6 + resolution: "husky@npm:9.1.6" bin: - husky: lib/bin.js - checksum: 837bc7e4413e58c1f2946d38fb050f5d7324c6f16b0fd66411ffce5703b294bd21429e8ba58711cd331951ee86ed529c5be4f76805959ff668a337dbfa82a1b0 + husky: bin.js + checksum: 421ccd8850378231aaefd70dbe9e4f1549b84ffe3a6897f93a202242bbc04e48bd498169aef43849411105d9fcf7c192b757d42661e28d06b934a609a4eb8771 languageName: node linkType: hard @@ -19689,7 +19689,7 @@ __metadata: glob: ^10.3.10 hls.js: ^1.4.12 html2canvas: ^1.4.1 - husky: ^8.0.3 + husky: ^9.1.6 intro.js: ^7.0.1 jest: 29.4.3 jest-circus: ^29.5.0