import {z} from 'zod' export type GetApiLettingsSearchArgs = {'filter[areas]'?: string | undefined, 'filter[bedrooms]'?: string | undefined, 'filter[min_bedrooms]'?: string | undefined, 'filter[branch]'?: string | undefined, 'filter[features]'?: string | undefined, 'filter[include_land]'?: string | undefined, 'filter[max_price]'?: string | undefined, 'filter[min_price]'?: string | undefined, 'filter[postcode]'?: string | undefined, 'filter[status]'?: (Array<'not_on_the_market' | 'valuation_lost' | 'valuation_booked' | 'valuation_pending' | 'valuation_cancelled' | 'provisional_valuation_booked' | 'valuation_completed' | 'instructed' | 'for_sale' | 'under_offer' | 'sold_stc' | 'exchanged' | 'let' | 'to_let' | 'let_agreed' | 'for_sale_and_to_let' | 'withdrawn_sales' | 'withdrawn_lettings' | 'withdrawn_sales_and_lettings' | 'completed' | 'auction_completed'>) | undefined, 'filter[tags]'?: string | undefined, include?: (Array<'branch' | 'rooms' | 'images' | 'floorplans' | 'featuresForPortals' | 'outsideSpaces' | 'parkingSpaces' | 'epc' | 'epc.pdf' | 'epc.image' | 'additionalMedia' | 'tags' | 'brochure' | 'development' | 'rooms.media'>) | undefined, 'page[number]'?: number | undefined, 'page[size]'?: number | undefined, sort?: (Array<'price' | '-price' | 'created_at' | '-created_at'>) | undefined, 'filter[include_archived]'?: string | undefined}; export type GetApiLettingsSearchOptions = {baseUrl?: string | undefined, yourApiToken?: string | undefined, fetch?: typeof fetch | undefined}; export const propertyUrl = z.object({media_type: z.enum(["audio_tour", "brochure", "epc", "epc_graph", "floor_plan", "image", "ipack", "virtual_tour"]).optional(), media_url: z.string().optional()}); export const links = z.object({self: z.string().optional(), first: z.string().optional(), next: z.string().optional(), prev: z.string().optional(), last: z.string().optional()}); export const toOneRelationship = z.object({data: z.object({id: z.string(), type: z.string()}).nullable().optional(), links: links.optional()}); export const toManyRelationship = z.object({data: z.array(z.object({id: z.string(), type: z.string()})).optional(), links: links.optional()}); export const property = z.object({type: z.string(), id: z.string(), attributes: z.object({branch_uuid: z.string().optional(), inline_address: z.string().optional(), public_address: z.string().optional(), postcode: z.string().optional(), bedrooms: z.number().int().gte(1).optional(), bathrooms: z.number().int().gte(1).optional(), receptions: z.number().int().optional(), floor_area: z.number().int().gte(1).nullable().optional(), plot_area: z.number().int().gte(1).nullable().optional(), land_area: z.number().gte(1).nullable().optional(), property_type: z.enum(["Not Specified", "Other", "House", "Mews House", "Cluster House", "Ground Floor Flat", "Flat", "Studio Flat", "Ground Floor Maisonette", "Maisonette", "Bungalow", "Dormer Bungalow", "Mobile Home", "Land (Residential)", "Link Detached", "Town House", "Cottage", "Chalet", "Chalet Bungalow", "Character Property", "House Unspecified", "Villa", "Apartment", "Penthouse", "Finca", "Barn Conversion", "Serviced Apartment", "Parking", "Sheltered Housing", "Retirement Property", "House Share", "Flat Share", "Park Home", "Garages", "Farm House", "Equestrian Facility", "Duplex", "Triplex", "Longere", "Gite", "Barn", "Trulli", "Mill", "Ruins", "Restaurant", "Cafe", "Castle", "Village House", "Cave House", "Cortijo", "Farm Land", "Plot", "Country House", "Stone House", "Caravan", "Lodge", "Log Cabin", "Manor House", "Stately Home", "Off Plan", "Detached Villa", "Bar Nightclub", "Shop", "Riad", "House Boat", "Hotel Room", "Block Of Apartments", "Private Halls", "Office", "Business Park", "Serviced Office", "Retail Property (High Street)", "Retail Property (Out Of Town)", "Convenience Store", "Hairdresser Barber Shop", "Hotel", "Petrol Station", "Post Office", "Pub", "Workshop Retail Space", "Distribution Warehouse", "Factory", "Heavy Industrial", "Industrial Park", "Light Industrial", "Storage", "Showroom", "Warehouse", "Land (Commercial)", "Commercial Development", "Industrial Development", "Residential Development", "Commercial Property", "Data Centre", "Farm", "Healthcare Facility", "Marine Property", "Mixed Use", "Research Development Facility", "Science Park", "Guest House", "Hospitality", "Leisure Facility", "Takeaway", "Childcare Facility", "Smallholding", "Place Of Worship", "Trade Counter", "Coach House", "House Of Multiple Occupation", "Sports Facilities", "Spa", "Campsite Holiday Village", "Chateau", "Farm Commercial", "Land", "Office (Commercial)", "Industrial (Commercial)", "Retail (Commercial)", "Farm (Commercial)", "Farm Residential", "Other (Commercial)"]).optional(), property_style: z.enum(["null", "None", "Detached", "Semi Detached", "Terraced", "Mid-Terraced", "End of Terrace", "Link Detached"]).nullable().optional(), is_commercial: z.boolean().optional(), property_age_bracket: z.enum(["null", "Unspecified", "New Build", "2020s", "2010s", "2000s", "1990s", "1970 - 1990", "1960 - 1970", "1940 - 1960", "1910 - 1940", "Edwardian (1901 - 1910)", "Victorian (1830 - 1901)", "Georgian (1710 - 1830)", "Pre-Georgian (pre 1710)"]).nullable().optional(), construction_year: z.number().int().nullable().optional(), status: z.enum(["Not On The Market", "Valuation Lost", "Valuation Booked", "Valuation Pending", "Valuation Cancelled", "Provisional Valuation Booked", "Valuation Completed", "Instructed", "For Sale", "Under Offer", "Sold STC", "Exchanged", "Let", "To Let", "Let Agreed", "For Sale and To Let", "Withdrawn Sales", "Withdrawn Lettings", "Withdrawn Sales And Lettings", "Completed"]).optional(), sale_status: z.enum(["null", "Not On The Market", "Valuation Lost", "Valuation Booked", "Valuation Pending", "Valuation Cancelled", "Provisional Valuation Booked", "Valuation Completed", "Instructed", "For Sale", "Under Offer", "Sold STC", "Exchanged", "For Sale and To Let", "Withdrawn Sales", "Withdrawn Sales And Lettings", "Completed"]).nullable().optional(), lettings_status: z.enum(["null", "Not On The Market", "Valuation Lost", "Valuation Booked", "Valuation Pending", "Valuation Cancelled", "Provisional Valuation Booked", "Valuation Completed", "Instructed", "Under Offer", "Let", "To Let", "Let Agreed", "For Sale and To Let", "Withdrawn Lettings", "Withdrawn Sales And Lettings"]).nullable().optional(), dual_listing: z.boolean().optional(), owner_label: z.enum(["Landlord", "Vendor", "Owner"]).optional(), tenure: z.enum(["Leasehold", "Freehold", "Commonhold", "Share of Freehold", "Flying Freehold", "Share Transfer"]).nullable().optional(), tenure_notes: z.string().nullable().optional(), lease_expiry_year: z.string().nullable().optional(), lease_expiry_date: z.string().nullable().optional(), public_url: z.string().optional(), viewing_booking_url: z.string().nullable().optional(), last_instructed_sales_at: z.string().nullable().optional(), last_instructed_lettings_at: z.string().nullable().optional(), last_exchanged_at: z.string().nullable().optional(), last_completed_at: z.string().nullable().optional(), created_at: z.string().optional(), updated_at: z.string().optional(), custom_meta_data: z.array(z.object({meta_name: z.string().optional()})).optional(), property_urls: z.array(propertyUrl).optional()}), relationships: z.object({address: toOneRelationship.optional(), details: toOneRelationship.optional(), features: toManyRelationship.optional(), rooms: toManyRelationship.optional(), outsideSpaces: toManyRelationship.optional(), parkingSpaces: toManyRelationship.optional(), development: toOneRelationship.optional(), salesListing: toOneRelationship.optional(), lettingsListing: toOneRelationship.optional(), primaryImage: toOneRelationship.optional(), images: toManyRelationship.optional(), floorplans: toManyRelationship.optional(), brochure: toOneRelationship.optional(), additionalMedia: toManyRelationship.optional(), epc: toOneRelationship.optional(), tags: toManyRelationship.optional()})}); export const address = z.object({type: z.string(), id: z.string(), attributes: z.object({anon_address: z.string().optional(), line_1: z.string().optional(), line_2: z.string().optional(), line_3: z.string().optional(), line_4: z.string().nullable().optional(), line_5: z.string().nullable().optional(), town: z.string().optional(), country: z.string().optional(), postcode: z.string().optional(), inline: z.string().optional(), longitude: z.number().nullable().optional(), latitude: z.number().nullable().optional(), royal_mail: z.object({postcode: z.string().optional(), post_town: z.string().optional(), thoroughfare: z.string().optional(), building_name: z.string().nullable().optional(), building_number: z.string().nullable().optional(), department_name: z.string().nullable().optional(), organisation_name: z.string().nullable().optional(), sub_building_name: z.string().nullable().optional(), dependent_locality: z.string().nullable().optional(), dependent_thoroughfare: z.string().nullable().optional(), double_dependent_locality: z.string().nullable().optional()}).nullable().optional()})}); export const branch = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional(), public_name: z.string().optional(), email_address: z.string().optional(), telephone: z.string().optional(), website: z.string().optional(), about_branch_copy: z.string().optional(), disclaimer: z.string().nullable().optional(), address: address.optional()})}); export const details = z.object({type: z.string(), id: z.string(), attributes: z.object({display_property_style: z.enum(["None", "Other", "Mid-Terraced", "End of Terrace", "Town House", "Mews", "Barn Conversion", "Cottage", "Farm House", "Maisonette", "Studio", "Bungalow", "Chalet Bungalow", "Link Detached", "Ground Floor Flat", "Castle", "Park Home", "Hotel", "Guest House", "Dormer", "Equestrian", "Penthouse"]).nullable().optional(), work_required: z.boolean().nullable().optional(), heating_system: z.enum(["Electric Boiler", "Gas Boiler", "Storage Heaters", "Oil", "Gas Tank", "Air Source", "Electric Heating", "Other"]).nullable().optional(), council_tax_band: z.string().nullable().optional(), council_tax_cost: z.number().int().nullable().optional(), local_authority: z.string().nullable().optional(), service_charge: z.number().int().nullable().optional(), service_charge_period: z.enum(["Month", "Year"]).nullable().optional(), service_charge_notes: z.string().nullable().optional(), ground_rent: z.number().int().nullable().optional(), ground_rent_period: z.enum(["Month", "Year"]).nullable().optional(), ground_rent_review_period_years: z.number().int().nullable().optional(), ground_rent_uplift: z.number().int().gte(0).lte(100).nullable().optional(), ground_rent_expiry: z.string().nullable().optional(), full_description: z.string().nullable().optional(), full_description_lettings: z.string().nullable().optional(), short_description: z.string().nullable().optional(), short_description_lettings: z.string().nullable().optional(), location_summary: z.string().nullable().optional(), agency_registration_number: z.string().nullable().optional(), has_parking: z.boolean().nullable().optional(), has_outdoor_space: z.boolean().nullable().optional(), virtual_tour: z.string().nullable().optional(), shared_ownership: z.boolean().optional(), shared_ownership_notes: z.string().nullable().optional(), shared_ownership_rent: z.number().int().nullable().optional(), shared_ownership_rent_frequency: z.enum(["Month", "Year"]).nullable().optional(), shared_ownership_percentage_sold: z.number().int().nullable().optional(), material_information: z.object({council_tax_band: z.string().max(1).nullable().optional(), lease_expiry_date: z.string().nullable().optional(), ground_rent_pence: z.number().int().nullable().optional(), ground_rent_period: z.string().nullable().optional(), ground_rent_expiry: z.string().nullable().optional(), ground_rent_review_period_years: z.number().int().nullable().optional(), ground_rent_uplift: z.number().int().nullable().optional(), service_charge_pence: z.number().int().nullable().optional(), service_charge_period: z.string().nullable().optional(), service_charge_notes: z.string().nullable().optional(), water_supply: z.array(z.enum(["Direct mains water", "Wells", "Boreholes", "Springs", "Other"])).nullable().optional(), water_supply_other: z.string().nullable().optional(), electricity_supply: z.array(z.enum(["National grid", "Wind turbine(s)", "Solar PV (Photovoltaic) panels", "Generator/private supply", "Other"])).nullable().optional(), electricity_supply_other: z.string().nullable().optional(), sewerage: z.array(z.enum(["Standard UK domestic", "Septic tank", "Domestic/small sewage treatment plants", "Cesspit", "Cesspool", "Other"])).nullable().optional(), sewerage_other: z.string().nullable().optional(), heating_supply: z.array(z.enum(["Central heating (electric)", "Central heating (gas)", "Other gas heating system", "Communal heating system", "LPG/oil central heating", "LPG central heating", "Oil central heating", "Wood burner/open fire", "Biomass boiler", "Solar panels and related technology", "Ground source heat pump", "Air source heat pump", "Other"])).nullable().optional(), heating_supply_other: z.string().nullable().optional(), heating_system: z.enum(["Electric Boiler", "Gas Boiler", "Storage Heaters", "Oil", "Gas Tank", "Air Source", "Electric Heating", "Other"]).nullable().optional(), broadband: z.string().nullable().optional(), has_restrictions: z.boolean().nullable().optional(), has_required_access: z.boolean().nullable().optional(), public_right_of_way: z.boolean().nullable().optional(), sources_of_flooding: z.array(z.unknown()).nullable().optional(), sources_of_flooding_other: z.string().nullable().optional(), has_flood_defences: z.boolean().nullable().optional(), has_flooded_in_last_five_years: z.boolean().nullable().optional(), adaptations_list: z.array(z.unknown()).nullable().optional(), adaptations_other: z.string().nullable().optional()}).optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const salesListing = z.object({type: z.string(), id: z.string(), attributes: z.object({status: z.enum(["For Sale", "Sold STC", "Withdrawn", "Under Offer"]).optional(), price: z.number().int().optional(), price_qualifier: z.enum(["None", "In Excess of", "Prices From", "Guide Price", "Offers Over", "Offers in Region of", "Fixed Price", "Price On Application", "Part Buy, Part Rent"]).nullable().optional(), display_price: z.boolean().optional(), archived: z.boolean().optional(), is_low_profile: z.boolean().optional(), is_auction: z.boolean().optional(), publish_after: z.string().optional(), new_home: z.boolean().optional(), retirement: z.boolean().optional(), occupancy_status: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const lettingsListing = z.object({type: z.string(), id: z.string(), attributes: z.object({status: z.enum(["To Let", "Let Agreed", "Let", "Withdrawn"]).optional(), price_pcm: z.number().int().nullable().optional(), deposit: z.number().int().nullable().optional(), display_price: z.boolean().optional(), archived: z.boolean().optional(), furnished: z.enum(["Unfurnished", "Fully Furnished", "Part Furnished"]).optional(), allows_pets: z.boolean().nullable().optional(), is_low_profile: z.boolean().optional(), publish_after: z.string().optional(), new_home: z.boolean().optional(), retirement: z.boolean().optional(), student_rental: z.boolean().optional(), min_tenancy_period: z.number().nullable().optional(), available_from: z.string().nullable().optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const room = z.object({type: z.string(), id: z.string(), attributes: z.object({room_type: z.enum(["bedroom", "bathroom", "kitchen", "dining room", "lounge", "garage", "utility room", "wc", "en-suite", "reception", "landing", "lounge / diner", "conservatory", "hallway", "basement", "dressing room", "office", "attic / loft space", "other"]).optional(), name: z.string().optional(), width: z.number().int().nullable().optional(), length: z.number().int().nullable().optional(), formatted_dimensions: z.string().nullable().optional(), description: z.string().nullable().optional(), order: z.string().optional(), created_at: z.string().optional(), updated_at: z.string().optional()}), relationships: z.object({media: toManyRelationship.optional()}).optional()}); export const media = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional(), order: z.number().int().optional(), is_featured: z.boolean().optional(), feature_index: z.number().int().nullable().optional(), title: z.string().nullable().optional(), media_type: z.string().nullable().optional(), is_image: z.boolean().optional(), url: z.string().optional(), urls: z.object({thumbnail: z.string().optional(), small: z.string().optional(), medium: z.string().optional(), large: z.string().optional(), hero: z.string().optional(), full: z.string().optional()}).optional()})}); export const floorplan = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional(), order: z.number().int().optional(), is_featured: z.boolean().optional(), feature_index: z.number().int().nullable().optional(), title: z.string().nullable().optional(), media_type: z.string().nullable().optional(), is_image: z.boolean().optional(), url: z.string().optional(), urls: z.object({thumbnail: z.string().optional(), small: z.string().optional(), medium: z.string().optional(), large: z.string().optional(), hero: z.string().optional(), full: z.string().optional()}).optional()})}); export const feature = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional()})}); export const outsideSpace = z.object({type: z.string(), id: z.string(), attributes: z.object({type: z.enum(["Garden", "Front Garden", "Rear Garden", "Communal Garden", "Yard", "Roof Terrace", "Balcony"]).optional(), space_type: z.enum(["Garden", "Front Garden", "Rear Garden", "Communal Garden", "Yard", "Roof Terrace", "Balcony"]).optional(), description: z.string().optional(), width: z.number().int().nullable().optional(), length: z.number().int().nullable().optional(), area: z.number().int().nullable().optional(), orientation: z.enum(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]).nullable().optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const parkingSpace = z.object({type: z.string(), id: z.string(), attributes: z.object({parking_space_type: z.enum(["Garage", "Car Port", "Off Road", "On Road", "Permit", "Secure Gated", "Allocated Parking", "On Drive"]).optional(), capacity: z.number().int().nullable().optional(), description: z.string().optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const epc = z.object({type: z.string(), id: z.string(), attributes: z.object({rating: z.string().optional(), report_url: z.string().nullable().optional(), energy_efficiency_current: z.string().optional(), energy_efficiency_potential: z.string().optional(), environment_impact_current: z.string().optional(), environment_impact_potential: z.string().optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const additionalMedia = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional(), order: z.number().int().optional(), is_featured: z.boolean().optional(), feature_index: z.number().int().nullable().optional(), title: z.string().nullable().optional(), media_type: z.string().nullable().optional(), is_image: z.boolean().optional(), url: z.string().optional(), urls: z.object({thumbnail: z.string().optional(), small: z.string().optional(), medium: z.string().optional(), large: z.string().optional(), hero: z.string().optional(), full: z.string().optional()}).optional()})}); export const tag = z.object({type: z.string(), id: z.string(), attributes: z.object({tag: z.string().optional()})}); export const brochure = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional(), order: z.number().int().optional(), is_featured: z.boolean().optional(), feature_index: z.number().int().nullable().optional(), title: z.string().nullable().optional(), is_image: z.boolean().optional(), url: z.string().optional()})}); export const development = z.object({type: z.string(), id: z.string(), attributes: z.object({name: z.string().optional(), description: z.string().optional(), created_at: z.string().optional(), updated_at: z.string().optional()})}); export const propertyIncludes = z.array(z.union([branch, address, details, salesListing, lettingsListing, room, media, floorplan, feature, outsideSpace, parkingSpace, epc, additionalMedia, tag, brochure, development])); export const pagination = z.object({pagination: z.object({total: z.number().int().optional(), count: z.number().int().optional(), per_page: z.number().int().optional(), current_page: z.number().int().optional(), total_pages: z.number().int().optional()}).optional()}); export const getApiLettingsSearchResponse = z.object({data: z.array(property).optional(), included: propertyIncludes.optional(), meta: pagination.optional(), links: links.optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Search for Lettings Properties * * GET /lettings/search */ export const getApiLettingsSearch = async (args: GetApiLettingsSearchArgs, options?: GetApiLettingsSearchOptions) => { const {baseUrl = "https://street.co.uk/api/property-feed/v1", fetch: fetchFn = fetch, yourApiToken} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('accept', "application/vnd.api+json") if (args['filter[areas]'] != null) query.append("filter[areas]", String(args['filter[areas]'])) if (args['filter[bedrooms]'] != null) query.append("filter[bedrooms]", String(args['filter[bedrooms]'])) if (args['filter[min_bedrooms]'] != null) query.append("filter[min_bedrooms]", String(args['filter[min_bedrooms]'])) if (args['filter[branch]'] != null) query.append("filter[branch]", String(args['filter[branch]'])) if (args['filter[features]'] != null) query.append("filter[features]", String(args['filter[features]'])) if (args['filter[include_land]'] != null) query.append("filter[include_land]", String(args['filter[include_land]'])) if (args['filter[max_price]'] != null) query.append("filter[max_price]", String(args['filter[max_price]'])) if (args['filter[min_price]'] != null) query.append("filter[min_price]", String(args['filter[min_price]'])) if (args['filter[postcode]'] != null) query.append("filter[postcode]", String(args['filter[postcode]'])) if (args['filter[status]'] != null) query.append("filter[status]", args['filter[status]'].join(',')) if (args['filter[tags]'] != null) query.append("filter[tags]", String(args['filter[tags]'])) if (args.include != null) query.append("include", args.include.join(',')) if (args['page[number]'] != null) query.append("page[number]", String(args['page[number]'])) if (args['page[size]'] != null) query.append("page[size]", String(args['page[size]'])) if (args.sort != null) query.append("sort", args.sort.join(',')) if (args['filter[include_archived]'] != null) query.append("filter[include_archived]", String(args['filter[include_archived]'])) if (yourApiToken !== undefined) headers.set('authorization', `Bearer ${yourApiToken}`) const url = `${baseUrl.replace(/\/+$/, '')}/lettings/search${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "GET", headers}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(getApiLettingsSearchResponse, await res.json()) };