fix: JSON support

This commit is contained in:
orion 2024-03-27 12:20:33 -05:00
parent e2eb753317
commit 753d14fdd9
Signed by: orion
GPG Key ID: 6D4165AE4C928719
9 changed files with 329 additions and 64 deletions

View File

@ -1,2 +1,2 @@
bun 1.0.35
bun 1.0.11
purescript 0.15.15

BIN
bun.lockb

Binary file not shown.

202
package-lock.json generated Normal file
View File

@ -0,0 +1,202 @@
{
"name": "purs",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "purs",
"dependencies": {
"pg": "^8.11.3"
},
"devDependencies": {
"bun-types": "1.0.11",
"purs-tidy": "^0.10.0",
"typescript": "^5.0.0"
},
"peerDependencies": {
"postgres-range": "^1.1.4"
}
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/bun-types": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.11.tgz",
"integrity": "sha512-XaDwjnBlkdTOtBEAcXhDnPSKFMlwFK/526z0iyairYIDhZJMzZM1QU4D7XRiEI2SpKQWexn0S/LN9Mwx5xSJNg==",
"dev": true
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/pg": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.6.2",
"pg-pool": "^3.6.1",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-range": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
"peer": true
},
"node_modules/purs-tidy": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/purs-tidy/-/purs-tidy-0.10.1.tgz",
"integrity": "sha512-i1QvMaDEaZXv/GWZNFWs5CISiBOkwPhG4D1S4Rw6zUCGaE+NQNWTjvwY21rifynGa2N2TiBJRC61LkORbmGxrA==",
"dev": true,
"bin": {
"purs-tidy": "bin/index.js"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/typescript": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
}
}
}

View File

@ -8,12 +8,11 @@
},
"devDependencies": {
"bun-types": "1.0.11",
"postgres-range": "^1.1.4",
"purs-tidy": "^0.10.0",
"spago": "next"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": { "pg-types": "^4.0.2" }
"dependencies": {
"postgres-range": "^1.1.4",
"pg": "^8.11.3"
}
}

View File

@ -12,13 +12,16 @@ workspace:
- foreign
- lists
- maybe
- mmorph
- newtype
- node-buffer
- precise-datetime
- prelude
- simple-json
- transformers
- unsafe-coerce
test_dependencies:
- foreign-object
- quickcheck
- spec
- spec-quickcheck
@ -45,6 +48,7 @@ workspace:
- fixed-points
- foldable-traversable
- foreign
- foreign-object
- fork
- formatters
- free
@ -80,6 +84,7 @@ workspace:
- record
- refs
- safe-coerce
- simple-json
- spec
- spec-quickcheck
- st
@ -88,9 +93,11 @@ workspace:
- transformers
- tuples
- type-equality
- typelevel-prelude
- unfoldable
- unicode
- unsafe-coerce
- variant
extra_packages: {}
packages:
aff:
@ -333,6 +340,23 @@ packages:
- prelude
- strings
- transformers
foreign-object:
type: registry
version: 4.1.0
integrity: sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo=
dependencies:
- arrays
- foldable-traversable
- functions
- gen
- lists
- maybe
- prelude
- st
- tailrec
- tuples
- typelevel-prelude
- unfoldable
fork:
type: registry
version: 6.0.0
@ -735,6 +759,20 @@ packages:
integrity: sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU=
dependencies:
- unsafe-coerce
simple-json:
type: registry
version: 9.0.0
integrity: sha256-K3RJaThqsszTd+TEklzZmAdDqvIHWgXIfKqlsoykU1c=
dependencies:
- arrays
- exceptions
- foreign
- foreign-object
- nullable
- prelude
- record
- typelevel-prelude
- variant
spec:
type: registry
version: 7.6.0
@ -852,6 +890,13 @@ packages:
version: 4.0.1
integrity: sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw=
dependencies: []
typelevel-prelude:
type: registry
version: 7.0.0
integrity: sha256-uFF2ph+vHcQpfPuPf2a3ukJDFmLhApmkpTMviHIWgJM=
dependencies:
- prelude
- type-equality
unfoldable:
type: registry
version: 6.0.0
@ -875,3 +920,16 @@ packages:
version: 6.0.0
integrity: sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0=
dependencies: []
variant:
type: registry
version: 8.0.0
integrity: sha256-SR//zQDg2dnbB8ZHslcxieUkCeNlbMToapvmh9onTtw=
dependencies:
- enums
- lists
- maybe
- partial
- prelude
- record
- tuples
- unsafe-coerce

View File

@ -13,15 +13,18 @@ package:
- foreign
- lists
- maybe
- mmorph
- newtype
- node-buffer
- precise-datetime
- prelude
- simple-json
- transformers
- unsafe-coerce
test:
main: Test.Main
dependencies:
- foreign-object
- quickcheck
- spec
- spec-quickcheck

View File

@ -1,4 +1,4 @@
import { getTypeParser, setTypeParser } from 'pg-types'
import * as Pg from 'pg'
import * as Range from 'postgres-range'
export const null_ = null
@ -20,18 +20,18 @@ export const modifyPgTypes = () => {
// @ts-ignore
const asString = a => a
const asStringArray = getTypeParser(oid['text[]'])
const asStringArray = Pg.types.getTypeParser(oid['text[]'])
const asStringRange = Range.parse
setTypeParser(oid['json'], asString)
setTypeParser(oid['jsonb'], asString)
setTypeParser(oid['json[]'], asStringArray)
setTypeParser(oid['jsonb[]'], asStringArray)
Pg.types.setTypeParser(oid['json'], asString)
Pg.types.setTypeParser(oid['jsonb'], asString)
Pg.types.setTypeParser(oid['json[]'], asStringArray)
Pg.types.setTypeParser(oid['jsonb[]'], asStringArray)
setTypeParser(oid['timestamp'], asString)
setTypeParser(oid['timestamptz'], asString)
setTypeParser(oid['timestamp[]'], asStringArray)
setTypeParser(oid['timestamptz[]'], asStringArray)
setTypeParser(oid['tsrange'], asStringRange)
setTypeParser(oid['tstzrange'], asStringRange)
Pg.types.setTypeParser(oid['timestamp'], asString)
Pg.types.setTypeParser(oid['timestamptz'], asString)
Pg.types.setTypeParser(oid['timestamp[]'], asStringArray)
Pg.types.setTypeParser(oid['timestamptz[]'], asStringArray)
Pg.types.setTypeParser(oid['tsrange'], asStringRange)
Pg.types.setTypeParser(oid['tstzrange'], asStringRange)
}

View File

@ -4,13 +4,14 @@ import Prelude
import Control.Alt ((<|>))
import Control.Monad.Error.Class (liftEither, liftMaybe)
import Control.Monad.Except (Except, ExceptT, except, runExcept, runExceptT, withExceptT)
import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.Morph (hoist)
import Data.Bifunctor (lmap)
import Data.DateTime (DateTime)
import Data.Generic.Rep (class Generic)
import Data.List.NonEmpty (NonEmptyList)
import Data.Maybe (Maybe(..))
import Data.Newtype (unwrap, wrap)
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Postgres.Raw (Raw)
import Data.Postgres.Raw (unsafeFromForeign, unsafeToForeign) as Raw
import Data.RFC3339String as DateTime.ISO
@ -18,19 +19,21 @@ import Data.Show.Generic (genericShow)
import Data.Traversable (traverse)
import Effect (Effect)
import Effect.Exception (error)
import Foreign (ForeignError)
import Foreign (ForeignError(..))
import Foreign as F
import Node.Buffer (Buffer)
import Simple.JSON (class ReadForeign, class WriteForeign, readJSON', writeJSON)
newtype JSON = JSON String
newtype JSON a = JSON a
derive instance Newtype (JSON a) _
derive newtype instance WriteForeign a => WriteForeign (JSON a)
derive newtype instance ReadForeign a => ReadForeign (JSON a)
foreign import null_ :: Raw
-- | Important! This effect MUST be evaluated to guarantee
-- | that (de)serialization will work for timestamp and JSON types.
-- |
-- | This mutates the `pg-types`, overriding the default deserialization
-- | behavior for JSON and timestamp types.
-- | This mutates `import('pg').types`, setting deserialization
-- | for some types to unmarshal as strings rather than JS values.
foreign import modifyPgTypes :: Effect Unit
-- | The SQL value NULL
@ -43,35 +46,12 @@ instance Show Null where
show = genericShow
-- | The serialization & deserialization monad.
type RepT a = ExceptT RepError Effect a
-- | Errors encounterable while serializing & deserializing.
data RepError
= RepErrorTypeMismatch { expected :: String, found :: String }
| RepErrorInvalid String
| RepErrorForeign ForeignError
| RepErrorOther String
| RepErrorMultiple (NonEmptyList RepError)
derive instance Generic RepError _
derive instance Eq RepError
instance Show RepError where
show a = genericShow a
instance Semigroup RepError where
append (RepErrorMultiple as) (RepErrorMultiple bs) = RepErrorMultiple (as <> bs)
append (RepErrorMultiple as) b = RepErrorMultiple (as <> pure b)
append a (RepErrorMultiple bs) = RepErrorMultiple (pure a <> bs)
append a b = RepErrorMultiple (pure a <> pure b)
type RepT a = ExceptT (NonEmptyList ForeignError) Effect a
-- | Flatten to an Effect, rendering any `RepError`s to `String` using `Show`.
smash :: forall a. RepT a -> Effect a
smash = liftEither <=< map (lmap (error <<< show)) <<< runExceptT
-- | Lift an `Except` returned by functions in the `Foreign` module to `RepT`
liftForeign :: forall a. Except (NonEmptyList ForeignError) a -> RepT a
liftForeign = except <<< runExcept <<< withExceptT (RepErrorMultiple <<< map RepErrorForeign)
-- | Serialize data of type `a` to a `Raw` SQL value.
class Serialize a where
serialize :: a -> RepT Raw
@ -95,6 +75,9 @@ instance Serialize Raw where
instance Serialize Null where
serialize _ = unsafeSerializeCoerce null_
instance WriteForeign a => Serialize (JSON a) where
serialize = serialize <<< writeJSON <<< unwrap
-- | `bytea`
instance Serialize Buffer where
serialize = unsafeSerializeCoerce
@ -127,36 +110,39 @@ instance Serialize a => Serialize (Array a) where
instance Deserialize Raw where
deserialize = pure
instance Deserialize Null where
deserialize = map (const Null) <<< F.readNullOrUndefined <<< Raw.unsafeToForeign
instance ReadForeign a => Deserialize (JSON a) where
deserialize = map wrap <<< (hoist (pure <<< unwrap) <<< readJSON') <=< deserialize @String
-- | `bytea`
instance Deserialize Buffer where
deserialize = liftForeign <<< (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign
instance Deserialize Null where
deserialize = map (const Null) <<< liftForeign <<< F.readNullOrUndefined <<< Raw.unsafeToForeign
deserialize = (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign
instance Deserialize Int where
deserialize = liftForeign <<< F.readInt <<< Raw.unsafeToForeign
deserialize = F.readInt <<< Raw.unsafeToForeign
instance Deserialize Boolean where
deserialize = liftForeign <<< F.readBoolean <<< Raw.unsafeToForeign
deserialize = F.readBoolean <<< Raw.unsafeToForeign
instance Deserialize String where
deserialize = liftForeign <<< F.readString <<< Raw.unsafeToForeign
deserialize = F.readString <<< Raw.unsafeToForeign
instance Deserialize Number where
deserialize = liftForeign <<< F.readNumber <<< Raw.unsafeToForeign
deserialize = F.readNumber <<< Raw.unsafeToForeign
instance Deserialize Char where
deserialize = liftForeign <<< F.readChar <<< Raw.unsafeToForeign
deserialize = F.readChar <<< Raw.unsafeToForeign
instance Deserialize DateTime where
deserialize raw = do
s :: String <- deserialize raw
let invalid = RepErrorInvalid $ "Not a valid ISO8601 string: `" <> s <> "`"
let invalid = pure $ ForeignError $ "Not a valid ISO8601 string: `" <> s <> "`"
liftMaybe invalid $ DateTime.ISO.toDateTime $ wrap s
instance Deserialize a => Deserialize (Array a) where
deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< liftForeign <<< F.readArray <<< Raw.unsafeToForeign
deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< F.readArray <<< Raw.unsafeToForeign
instance Deserialize a => Deserialize (Maybe a) where
deserialize raw =

View File

@ -8,8 +8,8 @@ import Data.DateTime (DateTime(..))
import Data.DateTime.Instant as Instant
import Data.Int as Int
import Data.Maybe (Maybe, fromJust, fromMaybe, maybe)
import Data.Newtype (wrap)
import Data.Postgres (class Rep, Null(..), deserialize, null_, serialize, smash)
import Data.Newtype (unwrap, wrap)
import Data.Postgres (class Rep, JSON(..), Null(..), deserialize, null_, serialize, smash)
import Data.Postgres.Range as Range
import Data.Postgres.Raw (Raw)
import Data.Postgres.Raw as Raw
@ -19,7 +19,9 @@ import Effect.Class (liftEffect)
import Effect.Console (log)
import Effect.Unsafe (unsafePerformEffect)
import Foreign (unsafeToForeign)
import Foreign.Object as Object
import Partial.Unsafe (unsafePartial)
import Simple.JSON (writeImpl, writeJSON)
import Test.QuickCheck (class Arbitrary, (==?))
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (shouldEqual)
@ -54,6 +56,21 @@ spec =
check @(Array String) "Array String" identity asRaw
check @DateTime "DateTime" dateTimeFromArbitrary (asRaw <<< DateTime.ISO.fromDateTime)
describe "JSON" do
describe "Record" do
it "deserialize" $
quickCheck \(a /\ b /\ c :: Int /\ String /\ Array {"foo" :: String}) -> unsafePerformEffect do
let
obj = {a, b, c}
json = writeJSON obj
act :: JSON _ <- smash $ deserialize $ asRaw json
pure $ obj ==? unwrap act
it "serialize" $
quickCheck \(a /\ b /\ c :: Int /\ String /\ Array {"foo" :: String}) -> unsafePerformEffect do
let obj = {a, b, c}
act <- smash $ serialize $ JSON obj
pure $ asRaw (writeJSON obj) ==? act
describe "Null" do
it "serialize" $ liftEffect $ shouldEqual null_ =<< (smash $ serialize Null)
it "deserialize" $ liftEffect $ shouldEqual Null =<< (smash $ deserialize null_)