Skip to content

Commit 0f7c80b

Browse files
necrophidiaPhillip Leonardoclaude
authored
Add new APIs, fix bugs, improve security, and add CI/CD (#7)
* Add new APIs, fix bugs, improve security, and add CI/CD - Add new functions: GetByKey, DeleteByKey, DeleteMultiByKey, InsertWithAutoKey, InsertMultiWithAutoKey, RunInTransaction, WithProject, Close, SetLogger - Add OpNotEqual filter operator - Fix Connect() returning partially initialized DB on error - Fix WithCursor() silently swallowing decode errors - Fix WithFilter() silently ignoring invalid FieldKey values - Fix Count() doc referencing non-existent From/Where API - Fix KeysOnly() doc referencing non-existent SelectKeys method - Fix data race on package-level logger with sync.RWMutex - Fix README install URL (yourusername -> louvri) - Rename ID-based APIs to Key-based for Datastore consistency - Add configurable Logger interface for structured logging - Add GitHub Actions: PR lint/build check and auto-release on merge - Add golangci-lint v2 configuration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix golangci-lint v2 config: move linters-settings under linters.settings, remove deprecated exclude-use-default Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add GitHub sponsor/funding configuration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Phillip Leonardo <phillip.leonardo@idexpress.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ba57b34 commit 0f7c80b

File tree

6 files changed

+448
-77
lines changed

6 files changed

+448
-77
lines changed

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ko_fi: necrophidia

.github/workflows/pr.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: PR Check
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
build-and-lint:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-go@v5
17+
with:
18+
go-version-file: go.mod
19+
20+
- name: Build
21+
run: go build ./...
22+
23+
- name: Vet
24+
run: go vet ./...
25+
26+
- name: Lint
27+
uses: golangci/golangci-lint-action@v7
28+
with:
29+
version: latest

.github/workflows/release.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
18+
- name: Get latest tag
19+
id: latest_tag
20+
run: |
21+
tag=$(git tag --sort=-v:refname | head -n 1)
22+
echo "tag=${tag:-v0.0.0}" >> "$GITHUB_OUTPUT"
23+
24+
- name: Bump patch version
25+
id: next_tag
26+
run: |
27+
tag="${{ steps.latest_tag.outputs.tag }}"
28+
# Strip leading 'v'
29+
version="${tag#v}"
30+
major=$(echo "$version" | cut -d. -f1)
31+
minor=$(echo "$version" | cut -d. -f2)
32+
patch=$(echo "$version" | cut -d. -f3)
33+
next_patch=$((patch + 1))
34+
next_tag="v${major}.${minor}.${next_patch}"
35+
echo "tag=${next_tag}" >> "$GITHUB_OUTPUT"
36+
37+
- name: Create tag and release
38+
env:
39+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40+
run: |
41+
git tag "${{ steps.next_tag.outputs.tag }}"
42+
git push origin "${{ steps.next_tag.outputs.tag }}"
43+
gh release create "${{ steps.next_tag.outputs.tag }}" \
44+
--generate-notes \
45+
--title "${{ steps.next_tag.outputs.tag }}"

.golangci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
version: "2"
2+
3+
linters:
4+
enable:
5+
- errcheck
6+
- govet
7+
- staticcheck
8+
- unused
9+
- ineffassign
10+
- gosec
11+
- gocritic
12+
settings:
13+
gosec:
14+
excludes:
15+
- G115
16+
17+
issues:
18+
max-issues-per-linter: 0
19+
max-same-issues: 0

README.md

Lines changed: 105 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ A type-safe, generic wrapper for Google Cloud Datastore in Go. Provides a fluent
1010
- **Batch operations** - Efficient multi-entity get, upsert, and delete
1111
- **Filter operators** - Type-safe enum for query operators
1212
- **Aggregation queries** - Efficient count operations without loading entities
13-
- **Auto-generated IDs** - Insert entities with Datastore-assigned IDs
13+
- **Auto-generated keys** - Insert entities with Datastore-assigned keys
14+
- **Projection queries** - Fetch only specific fields for efficiency
15+
- **Transaction support** - Atomic multi-entity operations
16+
- **Configurable logging** - Plug in your own structured logger
1417

1518
## Installation
1619

1720
```bash
18-
go get github.com/yourusername/dsx
21+
go get github.com/louvri/dsx
1922
```
2023

2124
## Quick Start
@@ -28,7 +31,7 @@ import (
2831
"log"
2932
"time"
3033

31-
"github.com/yourusername/dsx"
34+
"github.com/louvri/dsx"
3235
)
3336

3437
type User struct {
@@ -46,6 +49,7 @@ func main() {
4649
if err != nil {
4750
log.Fatal(err)
4851
}
52+
defer db.Close()
4953

5054
// Query users
5155
users, err := dsx.Query[User](db, ctx, "User").
@@ -76,6 +80,9 @@ db, err := dsx.Connect(ctx, "project-id", "database-id", "")
7680

7781
// Using explicit credentials JSON
7882
db, err := dsx.Connect(ctx, "project-id", "", credentialsJSON)
83+
84+
// Always close when done
85+
defer db.Close()
7986
```
8087

8188
### Querying
@@ -122,6 +129,7 @@ users, err := dsx.Query[User](db, ctx, "User").
122129
| `dsx.OpLessEqual` | Less than or equal (<=) |
123130
| `dsx.OpIn` | In list |
124131
| `dsx.OpNotIn` | Not in list |
132+
| `dsx.OpNotEqual` | Not equal (!=) |
125133

126134
#### Ordering
127135

@@ -156,7 +164,16 @@ if user == nil {
156164
}
157165
```
158166

159-
#### Get Multiple Entities by ID
167+
#### Get Single Entity by Key
168+
169+
```go
170+
user, err := dsx.GetByKey[User](db, ctx, "User", "user-123")
171+
if user == nil {
172+
// Not found
173+
}
174+
```
175+
176+
#### Get Multiple Entities by Key
160177

161178
```go
162179
users, err := dsx.GetMulti[User](db, ctx, "User", []string{"user-1", "user-2", "user-3"})
@@ -263,9 +280,9 @@ err := dsx.Query[User](db, ctx, "User").UpsertMulti(users)
263280

264281
> **Note:** Datastore limits batch operations to 500 entities.
265282
266-
#### Insert with Auto-generated ID
283+
#### Insert with Auto-generated Key
267284

268-
Use `InsertWithAutoID` when you want Datastore to generate a unique numeric ID and need to know the ID after insertion.
285+
Use `InsertWithAutoKey` when you want Datastore to generate a unique key and need to know it after insertion.
269286

270287
```go
271288
order := Order{
@@ -274,25 +291,37 @@ order := Order{
274291
CreatedAt: time.Now(),
275292
}
276293

277-
key, err := dsx.Query[Order](db, ctx, "Order").InsertWithAutoID(&order)
294+
key, err := dsx.Query[Order](db, ctx, "Order").InsertWithAutoKey(&order)
278295
if err != nil {
279296
return err
280297
}
281-
fmt.Printf("Created order with ID: %d\n", key.ID)
298+
fmt.Printf("Created order with key ID: %d\n", key.ID)
299+
```
300+
301+
#### Batch Insert with Auto-generated Keys
302+
303+
```go
304+
orders := []*Order{
305+
{CustomerID: "cust-1", Total: 10.00},
306+
{CustomerID: "cust-2", Total: 20.00},
307+
}
308+
309+
keys, err := dsx.Query[Order](db, ctx, "Order").InsertMultiWithAutoKey(orders)
282310
```
283311

284312
### Deleting
285313

286314
```go
315+
// Delete by key
316+
err := dsx.DeleteByKey(db, ctx, "User", "user-123")
317+
318+
// Delete multiple by keys
319+
err := dsx.DeleteMultiByKey(db, ctx, "User", []string{"user-1", "user-2", "user-3"})
320+
287321
// Delete by filter
288322
err := dsx.Query[User](db, ctx, "User").
289323
WithFilter("Status", dsx.OpEqual, "inactive").
290324
Delete()
291-
292-
// Delete specific entity
293-
err := dsx.Query[User](db, ctx, "User").
294-
WithFilter(dsx.FieldKey, dsx.OpEqual, "user-123").
295-
Delete()
296325
```
297326

298327
> **Warning:** Calling `Delete()` without filters will delete ALL entities of that kind.
@@ -309,6 +338,38 @@ employees, err := dsx.Query[Employee](db, ctx, "Employee").
309338
Select()
310339
```
311340

341+
#### Projection Queries
342+
343+
```go
344+
// Only fetch Name and Email fields
345+
users, err := dsx.Query[User](db, ctx, "User").
346+
WithProject("Name", "Email").
347+
Select()
348+
349+
// Combine with distinct
350+
users, err := dsx.Query[User](db, ctx, "User").
351+
WithProject("Status").
352+
WithDistinct().
353+
Select()
354+
```
355+
356+
> **Note:** Projected fields must be indexed. Properties with `noindex` tags cannot be projected.
357+
358+
#### Transactions
359+
360+
```go
361+
err := dsx.RunInTransaction(db, ctx, func(tx *datastore.Transaction) error {
362+
var user User
363+
key := datastore.NameKey("User", "user-123", nil)
364+
if err := tx.Get(key, &user); err != nil {
365+
return err
366+
}
367+
user.Balance += 100
368+
_, err := tx.Put(key, &user)
369+
return err
370+
})
371+
```
372+
312373
#### Distinct Results
313374

314375
```go
@@ -386,16 +447,16 @@ users, err := dsx.Query[User](db, ctx, "User").
386447
count := len(users)
387448
```
388449

389-
### Use GetMulti for Multiple Known IDs
450+
### Use GetMulti for Multiple Known Keys
390451

391452
```go
392453
// Good - single API call
393454
users, err := dsx.GetMulti[User](db, ctx, "User", []string{"user-1", "user-2", "user-3"})
394455

395456
// Bad - multiple API calls
396-
for _, id := range ids {
457+
for _, key := range keys {
397458
user, err := dsx.Query[User](db, ctx, "User").
398-
WithFilter(dsx.FieldKey, dsx.OpEqual, id).
459+
WithFilter(dsx.FieldKey, dsx.OpEqual, key).
399460
Get()
400461
}
401462
```
@@ -423,8 +484,8 @@ users, err := dsx.Query[User](db, ctx, "User").
423484
err := dsx.Query[User](db, ctx, "User").UpsertMulti(usersMap)
424485

425486
// Bad - multiple API calls
426-
for id, user := range usersMap {
427-
err := dsx.Query[User](db, ctx, "User").Upsert(id, user)
487+
for key, user := range usersMap {
488+
err := dsx.Query[User](db, ctx, "User").Upsert(key, user)
428489
}
429490
```
430491

@@ -441,6 +502,25 @@ type User struct {
441502
}
442503
```
443504

505+
## Custom Logging
506+
507+
By default, dsx logs to the standard library's `log` package. You can provide your own logger by implementing the `Logger` interface:
508+
509+
```go
510+
type Logger interface {
511+
Println(v ...any)
512+
Printf(format string, v ...any)
513+
}
514+
```
515+
516+
```go
517+
// Use a custom logger
518+
dsx.SetLogger(myLogger)
519+
520+
// Reset to default
521+
dsx.SetLogger(nil)
522+
```
523+
444524
## Error Handling
445525

446526
The package logs errors with context before returning them:
@@ -450,7 +530,13 @@ datastore User select-error <error details>
450530
datastore User upsert-error <error details>
451531
datastore User delete get-all error <error details>
452532
datastore get-multi User error <error details>
453-
datastore Order insert-with-auto-id-error <error details>
533+
datastore Order insert-with-auto-key-error <error details>
534+
datastore User get-by-key error <error details>
535+
datastore User delete-by-key error <error details>
536+
datastore Order insert-multi-with-auto-key-error <error details>
537+
datastore User cursor-decode-error <error details>
538+
datastore User delete-multi-by-key error <error details>
539+
datastore transaction-error <error details>
454540
```
455541

456542
Common errors:

0 commit comments

Comments
 (0)