{"id":13494422,"url":"https://github.com/asdine/storm","last_synced_at":"2025-05-13T21:03:38.489Z","repository":{"id":37484140,"uuid":"49366798","full_name":"asdine/storm","owner":"asdine","description":"Simple and powerful toolkit for BoltDB","archived":false,"fork":false,"pushed_at":"2024-01-07T00:07:01.000Z","size":490,"stargazers_count":2075,"open_issues_count":64,"forks_count":141,"subscribers_count":44,"default_branch":"master","last_synced_at":"2025-04-28T11:58:05.168Z","etag":null,"topics":["boltdb","bucket","database","indexes","query-engine","storm","toolkit"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/asdine.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-01-10T12:55:59.000Z","updated_at":"2025-04-21T08:39:56.000Z","dependencies_parsed_at":"2024-06-18T11:11:36.959Z","dependency_job_id":"1e2af205-0549-4019-96c2-47c62fbe36cf","html_url":"https://github.com/asdine/storm","commit_stats":{"total_commits":334,"total_committers":25,"mean_commits":13.36,"dds":"0.24850299401197606","last_synced_commit":"23213e9525985cdd3f0ca079c19b78db7e67b76f"},"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asdine%2Fstorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asdine%2Fstorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asdine%2Fstorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/asdine%2Fstorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/asdine","download_url":"https://codeload.github.com/asdine/storm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254027875,"owners_count":22002128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["boltdb","bucket","database","indexes","query-engine","storm","toolkit"],"created_at":"2024-07-31T19:01:24.944Z","updated_at":"2025-05-13T21:03:38.424Z","avatar_url":"https://github.com/asdine.png","language":"Go","readme":"# Storm\n\n[![Build Status](https://travis-ci.org/asdine/storm.svg)](https://travis-ci.org/asdine/storm)\n[![GoDoc](https://godoc.org/github.com/asdine/storm?status.svg)](https://godoc.org/github.com/asdine/storm)\n\nStorm is a simple and powerful toolkit for [BoltDB](https://github.com/coreos/bbolt). Basically, Storm provides indexes, a wide range of methods to store and fetch data, an advanced query system, and much more.\n\nIn addition to the examples below, see also the [examples in the GoDoc](https://godoc.org/github.com/asdine/storm#pkg-examples).\n\n_For extended queries and support for [Badger](https://github.com/dgraph-io/badger), see also [Genji](https://github.com/asdine/genji)_\n\n## Table of Contents\n\n- [Getting Started](#getting-started)\n- [Import Storm](#import-storm)\n- [Open a database](#open-a-database)\n- [Simple CRUD system](#simple-crud-system)\n  - [Declare your structures](#declare-your-structures)\n  - [Save your object](#save-your-object)\n    - [Auto Increment](#auto-increment)\n  - [Simple queries](#simple-queries)\n    - [Fetch one object](#fetch-one-object)\n    - [Fetch multiple objects](#fetch-multiple-objects)\n    - [Fetch all objects](#fetch-all-objects)\n    - [Fetch all objects sorted by index](#fetch-all-objects-sorted-by-index)\n    - [Fetch a range of objects](#fetch-a-range-of-objects)\n    - [Fetch objects by prefix](#fetch-objects-by-prefix)\n    - [Skip, Limit and Reverse](#skip-limit-and-reverse)\n    - [Delete an object](#delete-an-object)\n    - [Update an object](#update-an-object)\n    - [Initialize buckets and indexes before saving an object](#initialize-buckets-and-indexes-before-saving-an-object)\n    - [Drop a bucket](#drop-a-bucket)\n    - [Re-index a bucket](#re-index-a-bucket)\n  - [Advanced queries](#advanced-queries)\n  - [Transactions](#transactions)\n  - [Options](#options)\n    - [BoltOptions](#boltoptions)\n    - [MarshalUnmarshaler](#marshalunmarshaler)\n      - [Provided Codecs](#provided-codecs)\n    - [Use existing Bolt connection](#use-existing-bolt-connection)\n    - [Batch mode](#batch-mode)\n- [Nodes and nested buckets](#nodes-and-nested-buckets)\n  - [Node options](#node-options)\n- [Simple Key/Value store](#simple-keyvalue-store)\n- [BoltDB](#boltdb)\n- [License](#license)\n- [Credits](#credits)\n\n## Getting Started\n\n```bash\nGO111MODULE=on go get -u github.com/asdine/storm/v3\n```\n\n## Import Storm\n\n```go\nimport \"github.com/asdine/storm/v3\"\n```\n\n## Open a database\n\nQuick way of opening a database\n\n```go\ndb, err := storm.Open(\"my.db\")\n\ndefer db.Close()\n```\n\n`Open` can receive multiple options to customize the way it behaves. See [Options](#options) below\n\n## Simple CRUD system\n\n### Declare your structures\n\n```go\ntype User struct {\n  ID int // primary key\n  Group string `storm:\"index\"` // this field will be indexed\n  Email string `storm:\"unique\"` // this field will be indexed with a unique constraint\n  Name string // this field will not be indexed\n  Age int `storm:\"index\"`\n}\n```\n\nThe primary key can be of any type as long as it is not a zero value. Storm will search for the tag `id`, if not present Storm will search for a field named `ID`.\n\n```go\ntype User struct {\n  ThePrimaryKey string `storm:\"id\"`// primary key\n  Group string `storm:\"index\"` // this field will be indexed\n  Email string `storm:\"unique\"` // this field will be indexed with a unique constraint\n  Name string // this field will not be indexed\n}\n```\n\nStorm handles tags in nested structures with the `inline` tag\n\n```go\ntype Base struct {\n  Ident bson.ObjectId `storm:\"id\"`\n}\n\ntype User struct {\n  Base      `storm:\"inline\"`\n  Group     string `storm:\"index\"`\n  Email     string `storm:\"unique\"`\n  Name      string\n  CreatedAt time.Time `storm:\"index\"`\n}\n```\n\n### Save your object\n\n```go\nuser := User{\n  ID: 10,\n  Group: \"staff\",\n  Email: \"john@provider.com\",\n  Name: \"John\",\n  Age: 21,\n  CreatedAt: time.Now(),\n}\n\nerr := db.Save(\u0026user)\n// err == nil\n\nuser.ID++\nerr = db.Save(\u0026user)\n// err == storm.ErrAlreadyExists\n```\n\nThat's it.\n\n`Save` creates or updates all the required indexes and buckets, checks the unique constraints and saves the object to the store.\n\n#### Auto Increment\n\nStorm can auto increment integer values so you don't have to worry about that when saving your objects. Also, the new value is automatically inserted in your field.\n\n```go\n\ntype Product struct {\n  Pk                  int `storm:\"id,increment\"` // primary key with auto increment\n  Name                string\n  IntegerField        uint64 `storm:\"increment\"`\n  IndexedIntegerField uint32 `storm:\"index,increment\"`\n  UniqueIntegerField  int16  `storm:\"unique,increment=100\"` // the starting value can be set\n}\n\np := Product{Name: \"Vaccum Cleaner\"}\n\nfmt.Println(p.Pk)\nfmt.Println(p.IntegerField)\nfmt.Println(p.IndexedIntegerField)\nfmt.Println(p.UniqueIntegerField)\n// 0\n// 0\n// 0\n// 0\n\n_ = db.Save(\u0026p)\n\nfmt.Println(p.Pk)\nfmt.Println(p.IntegerField)\nfmt.Println(p.IndexedIntegerField)\nfmt.Println(p.UniqueIntegerField)\n// 1\n// 1\n// 1\n// 100\n\n```\n\n### Simple queries\n\nAny object can be fetched, indexed or not. Storm uses indexes when available, otherwise it uses the [query system](#advanced-queries).\n\n#### Fetch one object\n\n```go\nvar user User\nerr := db.One(\"Email\", \"john@provider.com\", \u0026user)\n// err == nil\n\nerr = db.One(\"Name\", \"John\", \u0026user)\n// err == nil\n\nerr = db.One(\"Name\", \"Jack\", \u0026user)\n// err == storm.ErrNotFound\n```\n\n#### Fetch multiple objects\n\n```go\nvar users []User\nerr := db.Find(\"Group\", \"staff\", \u0026users)\n```\n\n#### Fetch all objects\n\n```go\nvar users []User\nerr := db.All(\u0026users)\n```\n\n#### Fetch all objects sorted by index\n\n```go\nvar users []User\nerr := db.AllByIndex(\"CreatedAt\", \u0026users)\n```\n\n#### Fetch a range of objects\n\n```go\nvar users []User\nerr := db.Range(\"Age\", 10, 21, \u0026users)\n```\n\n#### Fetch objects by prefix\n\n```go\nvar users []User\nerr := db.Prefix(\"Name\", \"Jo\", \u0026users)\n```\n\n#### Skip, Limit and Reverse\n\n```go\nvar users []User\nerr := db.Find(\"Group\", \"staff\", \u0026users, storm.Skip(10))\nerr = db.Find(\"Group\", \"staff\", \u0026users, storm.Limit(10))\nerr = db.Find(\"Group\", \"staff\", \u0026users, storm.Reverse())\nerr = db.Find(\"Group\", \"staff\", \u0026users, storm.Limit(10), storm.Skip(10), storm.Reverse())\n\nerr = db.All(\u0026users, storm.Limit(10), storm.Skip(10), storm.Reverse())\nerr = db.AllByIndex(\"CreatedAt\", \u0026users, storm.Limit(10), storm.Skip(10), storm.Reverse())\nerr = db.Range(\"Age\", 10, 21, \u0026users, storm.Limit(10), storm.Skip(10), storm.Reverse())\n```\n\n#### Delete an object\n\n```go\nerr := db.DeleteStruct(\u0026user)\n```\n\n#### Update an object\n\n```go\n// Update multiple fields\n// Only works for non zero-value fields (e.g. Name can not be \"\", Age can not be 0)\nerr := db.Update(\u0026User{ID: 10, Name: \"Jack\", Age: 45})\n\n// Update a single field\n// Also works for zero-value fields (0, false, \"\", ...)\nerr := db.UpdateField(\u0026User{ID: 10}, \"Age\", 0)\n```\n\n#### Initialize buckets and indexes before saving an object\n\n```go\nerr := db.Init(\u0026User{})\n```\n\nUseful when starting your application\n\n#### Drop a bucket\n\nUsing the struct\n\n```go\nerr := db.Drop(\u0026User)\n```\n\nUsing the bucket name\n\n```go\nerr := db.Drop(\"User\")\n```\n\n#### Re-index a bucket\n\n```go\nerr := db.ReIndex(\u0026User{})\n```\n\nUseful when the structure has changed\n\n### Advanced queries\n\nFor more complex queries, you can use the `Select` method.\n`Select` takes any number of [`Matcher`](https://godoc.org/github.com/asdine/storm/q#Matcher) from the [`q`](https://godoc.org/github.com/asdine/storm/q) package.\n\nHere are some common Matchers:\n\n```go\n// Equality\nq.Eq(\"Name\", John)\n\n// Strictly greater than\nq.Gt(\"Age\", 7)\n\n// Lesser than or equal to\nq.Lte(\"Age\", 77)\n\n// Regex with name that starts with the letter D\nq.Re(\"Name\", \"^D\")\n\n// In the given slice of values\nq.In(\"Group\", []string{\"Staff\", \"Admin\"})\n\n// Comparing fields\nq.EqF(\"FieldName\", \"SecondFieldName\")\nq.LtF(\"FieldName\", \"SecondFieldName\")\nq.GtF(\"FieldName\", \"SecondFieldName\")\nq.LteF(\"FieldName\", \"SecondFieldName\")\nq.GteF(\"FieldName\", \"SecondFieldName\")\n```\n\nMatchers can also be combined with `And`, `Or` and `Not`:\n\n```go\n\n// Match if all match\nq.And(\n  q.Gt(\"Age\", 7),\n  q.Re(\"Name\", \"^D\")\n)\n\n// Match if one matches\nq.Or(\n  q.Re(\"Name\", \"^A\"),\n  q.Not(\n    q.Re(\"Name\", \"^B\")\n  ),\n  q.Re(\"Name\", \"^C\"),\n  q.In(\"Group\", []string{\"Staff\", \"Admin\"}),\n  q.And(\n    q.StrictEq(\"Password\", []byte(password)),\n    q.Eq(\"Registered\", true)\n  )\n)\n```\n\nYou can find the complete list in the [documentation](https://godoc.org/github.com/asdine/storm/q#Matcher).\n\n`Select` takes any number of matchers and wraps them into a `q.And()` so it's not necessary to specify it. It returns a [`Query`](https://godoc.org/github.com/asdine/storm#Query) type.\n\n```go\nquery := db.Select(q.Gte(\"Age\", 7), q.Lte(\"Age\", 77))\n```\n\nThe `Query` type contains methods to filter and order the records.\n\n```go\n// Limit\nquery = query.Limit(10)\n\n// Skip\nquery = query.Skip(20)\n\n// Calls can also be chained\nquery = query.Limit(10).Skip(20).OrderBy(\"Age\").Reverse()\n```\n\nBut also to specify how to fetch them.\n\n```go\nvar users []User\nerr = query.Find(\u0026users)\n\nvar user User\nerr = query.First(\u0026user)\n```\n\nExamples with `Select`:\n\n```go\n// Find all users with an ID between 10 and 100\nerr = db.Select(q.Gte(\"ID\", 10), q.Lte(\"ID\", 100)).Find(\u0026users)\n\n// Nested matchers\nerr = db.Select(q.Or(\n  q.Gt(\"ID\", 50),\n  q.Lt(\"Age\", 21),\n  q.And(\n    q.Eq(\"Group\", \"admin\"),\n    q.Gte(\"Age\", 21),\n  ),\n)).Find(\u0026users)\n\nquery := db.Select(q.Gte(\"ID\", 10), q.Lte(\"ID\", 100)).Limit(10).Skip(5).Reverse().OrderBy(\"Age\", \"Name\")\n\n// Find multiple records\nerr = query.Find(\u0026users)\n// or\nerr = db.Select(q.Gte(\"ID\", 10), q.Lte(\"ID\", 100)).Limit(10).Skip(5).Reverse().OrderBy(\"Age\", \"Name\").Find(\u0026users)\n\n// Find first record\nerr = query.First(\u0026user)\n// or\nerr = db.Select(q.Gte(\"ID\", 10), q.Lte(\"ID\", 100)).Limit(10).Skip(5).Reverse().OrderBy(\"Age\", \"Name\").First(\u0026user)\n\n// Delete all matching records\nerr = query.Delete(new(User))\n\n// Fetching records one by one (useful when the bucket contains a lot of records)\nquery = db.Select(q.Gte(\"ID\", 10),q.Lte(\"ID\", 100)).OrderBy(\"Age\", \"Name\")\n\nerr = query.Each(new(User), func(record interface{}) error) {\n  u := record.(*User)\n  ...\n  return nil\n}\n```\n\nSee the [documentation](https://godoc.org/github.com/asdine/storm#Query) for a complete list of methods.\n\n### Transactions\n\n```go\ntx, err := db.Begin(true)\nif err != nil {\n  return err\n}\ndefer tx.Rollback()\n\naccountA.Amount -= 100\naccountB.Amount += 100\n\nerr = tx.Save(accountA)\nif err != nil {\n  return err\n}\n\nerr = tx.Save(accountB)\nif err != nil {\n  return err\n}\n\nreturn tx.Commit()\n```\n\n### Options\n\nStorm options are functions that can be passed when constructing you Storm instance. You can pass it any number of options.\n\n#### BoltOptions\n\nBy default, Storm opens a database with the mode `0600` and a timeout of one second.\nYou can change this behavior by using `BoltOptions`\n\n```go\ndb, err := storm.Open(\"my.db\", storm.BoltOptions(0600, \u0026bolt.Options{Timeout: 1 * time.Second}))\n```\n\n#### MarshalUnmarshaler\n\nTo store the data in BoltDB, Storm marshals it in JSON by default. If you wish to change this behavior you can pass a codec that implements [`codec.MarshalUnmarshaler`](https://godoc.org/github.com/asdine/storm/codec#MarshalUnmarshaler) via the [`storm.Codec`](https://godoc.org/github.com/asdine/storm#Codec) option:\n\n```go\ndb := storm.Open(\"my.db\", storm.Codec(myCodec))\n```\n\n##### Provided Codecs\n\nYou can easily implement your own `MarshalUnmarshaler`, but Storm comes with built-in support for [JSON](https://godoc.org/github.com/asdine/storm/codec/json) (default), [GOB](https://godoc.org/github.com/asdine/storm/codec/gob),  [Sereal](https://godoc.org/github.com/asdine/storm/codec/sereal), [Protocol Buffers](https://godoc.org/github.com/asdine/storm/codec/protobuf) and [MessagePack](https://godoc.org/github.com/asdine/storm/codec/msgpack).\n\nThese can be used by importing the relevant package and use that codec to configure Storm. The example below shows all variants (without proper error handling):\n\n```go\nimport (\n  \"github.com/asdine/storm/v3\"\n  \"github.com/asdine/storm/v3/codec/gob\"\n  \"github.com/asdine/storm/v3/codec/json\"\n  \"github.com/asdine/storm/v3/codec/sereal\"\n  \"github.com/asdine/storm/v3/codec/protobuf\"\n  \"github.com/asdine/storm/v3/codec/msgpack\"\n)\n\nvar gobDb, _ = storm.Open(\"gob.db\", storm.Codec(gob.Codec))\nvar jsonDb, _ = storm.Open(\"json.db\", storm.Codec(json.Codec))\nvar serealDb, _ = storm.Open(\"sereal.db\", storm.Codec(sereal.Codec))\nvar protobufDb, _ = storm.Open(\"protobuf.db\", storm.Codec(protobuf.Codec))\nvar msgpackDb, _ = storm.Open(\"msgpack.db\", storm.Codec(msgpack.Codec))\n```\n\n**Tip**: Adding Storm tags to generated Protobuf files can be tricky. A good solution is to use [this tool](https://github.com/favadi/protoc-go-inject-tag) to inject the tags during the compilation.\n\n#### Use existing Bolt connection\n\nYou can use an existing connection and pass it to Storm\n\n```go\nbDB, _ := bolt.Open(filepath.Join(dir, \"bolt.db\"), 0600, \u0026bolt.Options{Timeout: 10 * time.Second})\ndb := storm.Open(\"my.db\", storm.UseDB(bDB))\n```\n\n#### Batch mode\n\nBatch mode can be enabled to speed up concurrent writes (see [Batch read-write transactions](https://github.com/coreos/bbolt#batch-read-write-transactions))\n\n```go\ndb := storm.Open(\"my.db\", storm.Batch())\n```\n\n## Nodes and nested buckets\n\nStorm takes advantage of BoltDB nested buckets feature by using `storm.Node`.\nA `storm.Node` is the underlying object used by `storm.DB` to manipulate a bucket.\nTo create a nested bucket and use the same API as `storm.DB`, you can use the `DB.From` method.\n\n```go\nrepo := db.From(\"repo\")\n\nerr := repo.Save(\u0026Issue{\n  Title: \"I want more features\",\n  Author: user.ID,\n})\n\nerr = repo.Save(newRelease(\"0.10\"))\n\nvar issues []Issue\nerr = repo.Find(\"Author\", user.ID, \u0026issues)\n\nvar release Release\nerr = repo.One(\"Tag\", \"0.10\", \u0026release)\n```\n\nYou can also chain the nodes to create a hierarchy\n\n```go\nchars := db.From(\"characters\")\nheroes := chars.From(\"heroes\")\nenemies := chars.From(\"enemies\")\n\nitems := db.From(\"items\")\npotions := items.From(\"consumables\").From(\"medicine\").From(\"potions\")\n```\n\nYou can even pass the entire hierarchy as arguments to `From`:\n\n```go\nprivateNotes := db.From(\"notes\", \"private\")\nworkNotes :=  db.From(\"notes\", \"work\")\n```\n\n### Node options\n\nA Node can also be configured. Activating an option on a Node creates a copy, so a Node is always thread-safe.\n\n```go\nn := db.From(\"my-node\")\n```\n\nGive a bolt.Tx transaction to the Node\n\n```go\nn = n.WithTransaction(tx)\n```\n\nEnable batch mode\n\n```go\nn = n.WithBatch(true)\n```\n\nUse a Codec\n\n```go\nn = n.WithCodec(gob.Codec)\n```\n\n## Simple Key/Value store\n\nStorm can be used as a simple, robust, key/value store that can store anything.\nThe key and the value can be of any type as long as the key is not a zero value.\n\nSaving data :\n\n```go\ndb.Set(\"logs\", time.Now(), \"I'm eating my breakfast man\")\ndb.Set(\"sessions\", bson.NewObjectId(), \u0026someUser)\ndb.Set(\"weird storage\", \"754-3010\", map[string]interface{}{\n  \"hair\": \"blonde\",\n  \"likes\": []string{\"cheese\", \"star wars\"},\n})\n```\n\nFetching data :\n\n```go\nuser := User{}\ndb.Get(\"sessions\", someObjectId, \u0026user)\n\nvar details map[string]interface{}\ndb.Get(\"weird storage\", \"754-3010\", \u0026details)\n\ndb.Get(\"sessions\", someObjectId, \u0026details)\n```\n\nDeleting data :\n\n```go\ndb.Delete(\"sessions\", someObjectId)\ndb.Delete(\"weird storage\", \"754-3010\")\n```\n\nYou can find other useful methods in the [documentation](https://godoc.org/github.com/asdine/storm#KeyValueStore).\n\n## BoltDB\n\nBoltDB is still easily accessible and can be used as usual\n\n```go\ndb.Bolt.View(func(tx *bolt.Tx) error {\n  bucket := tx.Bucket([]byte(\"my bucket\"))\n  val := bucket.Get([]byte(\"any id\"))\n  fmt.Println(string(val))\n  return nil\n})\n```\n\nA transaction can be also be passed to Storm\n\n```go\ndb.Bolt.Update(func(tx *bolt.Tx) error {\n  ...\n  dbx := db.WithTransaction(tx)\n  err = dbx.Save(\u0026user)\n  ...\n  return nil\n})\n```\n\n## License\n\nMIT\n\n## Credits\n\n- [Asdine El Hrychy](https://github.com/asdine)\n- [Bjørn Erik Pedersen](https://github.com/bep)\n","funding_links":[],"categories":["Go","Utilities","公用事业公司","实用工具","實用工具","工具库","工具库`可以提升效率的通用代码库和工具`","ORM","Utility","\u003ca name=\"Go\"\u003e\u003c/a\u003eGo"],"sub_categories":["Utility/Miscellaneous","实用程序/Miscellaneous","HTTP Clients","Advanced Console UIs","高级控制台界面","高級控制台界面","查询语","Fail injection","交流","\u003cspan id=\"高级控制台用户界面-advanced-console-uis\"\u003e高级控制台用户界面 Advanced Console UIs\u003c/span\u003e"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasdine%2Fstorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fasdine%2Fstorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fasdine%2Fstorm/lists"}